Skip to content

0.9.12 #386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fd5b792
build and publish wheel
dimbleby Feb 12, 2025
4a80822
Add CostRate to TimeActivity
laf-rge Mar 16, 2025
7bc2973
add integration test
laf-rge Mar 16, 2025
c164821
add SKU to Item
laf-rge Mar 16, 2025
b54c36a
fix test cases
laf-rge Mar 16, 2025
71d975d
Fix SKU field retrieval in Item.all() and clean up test files
laf-rge Mar 16, 2025
67e94c2
use python 3 mock
laf-rge Mar 16, 2025
d2c811a
attachable bytes
laf-rge Mar 16, 2025
a77be22
use minorversion 75 by default
laf-rge Mar 16, 2025
9400256
updated test client to match new functionality
laf-rge Mar 16, 2025
c769b19
ensure minorversion is checked properly
laf-rge Mar 16, 2025
1dc7f68
cleanup version number default
laf-rge Mar 16, 2025
b4014b4
fixed for new message
laf-rge Mar 16, 2025
ff14b00
fix setup.cfg - -> _
laf-rge Mar 24, 2025
58c1855
Remove description-file. It is not supported by setuptools
romeroyonatan Mar 24, 2025
3d8d593
Merge pull request #376 from dimbleby/build-and-publish-wheel
ej2 Apr 15, 2025
04a0cd4
Merge pull request #378 from laf-rge/master
ej2 Apr 15, 2025
39abbef
Merge pull request #379 from laf-rge/sku-fix
ej2 Apr 15, 2025
5efc535
Merge pull request #380 from laf-rge/attachable-bytes
ej2 Apr 15, 2025
04d5563
Merge branch '0.9.12' into minor-version
ej2 Apr 15, 2025
a4d680f
Merge pull request #381 from laf-rge/minor-version
ej2 Apr 15, 2025
66996ba
Merge pull request #382 from romeroyonatan/patch-1
ej2 Apr 15, 2025
24c0dc5
Merge branch '0.9.12' into fix-setuptools
ej2 Apr 15, 2025
0145226
Merge pull request #384 from laf-rge/fix-setuptools
ej2 Apr 15, 2025
beab272
Update tests and fix issues
ej2 Apr 16, 2025
71693d0
Update version number
ej2 Apr 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

* 0.9.12 (April 15, 2025)
* Add CostRate to TimeActivity
* Fix retrieval of Item SKU field
* Added support for attaching files using byte streams
* Default minor version to minimum supported version
* Fix incompatibility issues with setuptools

* 0.9.11 (February 10, 2025)
* Add warning for unsupported minorversion
* Fix issue with new versions of jsonEncoder
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
publish: clean
python setup.py sdist
python -m build
twine upload dist/*

clean:
rm -vrf ./build ./dist ./*.egg-info
find . -name '*.pyc' -delete
find . -name '*.tgz' -delete
find . -name '*.tgz' -delete
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ urllib3 = ">=2.1.0"
intuit-oauth = "==1.2.6"
requests = ">=2.31.0"
requests_oauthlib = ">=1.3.1"
setuptools = "*"
build = "*"
36 changes: 26 additions & 10 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A Python 3 library for accessing the Quickbooks API. Complete rework of
[quickbooks-python](https://github.com/troolee/quickbooks-python).

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

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

Attaching file bytes to customer:

attachment = Attachable()

attachable_ref = AttachableRef()
attachable_ref.EntityRef = customer.to_ref()

attachment.AttachableRef.append(attachable_ref)

attachment.FileName = 'Filename'
attachment._FileBytes = pdf_bytes # bytes object containing the file content
attachment.ContentType = 'application/pdf'
attachment.save(qb=client)

**Note:** You can use either `_FilePath` or `_FileBytes` to attach a file, but not both at the same time.

Passing in optional params
----------------
Some QBO objects have options that need to be set on the query string of an API call.
Expand Down
1 change: 0 additions & 1 deletion dev_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
coverage==7.3.0
ipdb==0.13.13
mock==5.1.0
nose==1.3.7
63 changes: 40 additions & 23 deletions quickbooks/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,19 @@ def __new__(cls, **kwargs):
if 'company_id' in kwargs:
instance.company_id = kwargs['company_id']

if 'minorversion' in kwargs:
instance.minorversion = kwargs['minorversion']

if instance.minorversion < instance.MINIMUM_MINOR_VERSION:
warnings.warn(
'Minor Version no longer supported.'
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
DeprecationWarning)
# Handle minorversion with default
instance.minorversion = kwargs.get('minorversion', instance.MINIMUM_MINOR_VERSION)
if 'minorversion' not in kwargs:
warnings.warn(
'No minor version specified. Defaulting to minimum supported version (75). '
'Please specify minorversion explicitly when initializing QuickBooks. '
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
DeprecationWarning)
elif instance.minorversion < instance.MINIMUM_MINOR_VERSION:
warnings.warn(
f'Minor Version {instance.minorversion} is no longer supported. Minimum supported version is {instance.MINIMUM_MINOR_VERSION}. '
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
DeprecationWarning)

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

Expand Down Expand Up @@ -152,13 +157,12 @@ def change_data_capture(self, entity_string, changed_since):
return result

def make_request(self, request_type, url, request_body=None, content_type='application/json',
params=None, file_path=None, request_id=None):
params=None, file_path=None, file_bytes=None, request_id=None):

if not params:
params = {}

if self.minorversion:
params['minorversion'] = self.minorversion
params['minorversion'] = self.minorversion

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

if file_path:
if file_path or file_bytes:
url = url.replace('attachable', 'upload')
boundary = '-------------PythonMultipartPost'
headers.update({
Expand All @@ -183,8 +187,11 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
'Connection': 'close'
})

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

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

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

def get(self, *args, **kwargs):
return self.make_request("GET", *args, **kwargs)
if 'params' not in kwargs:
kwargs['params'] = {}

return self.make_request('GET', *args, **kwargs)

def post(self, *args, **kwargs):
return self.make_request("POST", *args, **kwargs)
if 'params' not in kwargs:
kwargs['params'] = {}

return self.make_request('POST', *args, **kwargs)

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

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, {}, params=params)
url = "{0}/company/{1}/{2}/{3}".format(self.api_url, self.company_id, qbbo.lower(), pk)
if params is None:
params = {}

return result
return self.get(url, {}, params=params)

@staticmethod
def handle_exceptions(results):
Expand Down Expand Up @@ -287,11 +301,11 @@ def handle_exceptions(results):
else:
raise exceptions.QuickbooksException(message, code, detail)

def create_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
def create_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
self.isvalid_object_name(qbbo)

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

return results

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

return True

def update_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
def update_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
result = self.post(url, request_body, file_path=_file_path, request_id=request_id, params=params)
if params is None:
params = {}

result = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params)

return result

Expand Down
28 changes: 20 additions & 8 deletions quickbooks/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,26 @@ class ListMixin(object):

@classmethod
def all(cls, order_by="", start_position="", max_results=100, qb=None):
"""
:param start_position:
:param max_results: The max number of entities that can be returned in a response is 1000.
:param qb:
:return: Returns list
"""
return cls.where("", order_by=order_by, start_position=start_position,
max_results=max_results, qb=qb)
"""Returns list of objects containing all objects in the QuickBooks database"""
if qb is None:
qb = QuickBooks()

# For Item objects, we need to explicitly request the SKU field
if cls.qbo_object_name == "Item":
select = "SELECT *, Sku FROM {0}".format(cls.qbo_object_name)
else:
select = "SELECT * FROM {0}".format(cls.qbo_object_name)

if order_by:
select += " ORDER BY {0}".format(order_by)

if start_position:
select += " STARTPOSITION {0}".format(start_position)

if max_results:
select += " MAXRESULTS {0}".format(max_results)

return cls.query(select, qb=qb)

@classmethod
def filter(cls, order_by="", start_position="", max_results="", qb=None, **kwargs):
Expand Down
13 changes: 11 additions & 2 deletions quickbooks/objects/attachable.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self):
self.AttachableRef = []
self.FileName = None
self._FilePath = ''
self._FileBytes = None
self.Note = ""
self.FileAccessUri = None
self.TempDownloadUri = None
Expand All @@ -53,10 +54,18 @@ def save(self, qb=None):
if not qb:
qb = QuickBooks()

# Validate that we have either file path or bytes, but not both
if self._FilePath and self._FileBytes:
raise ValueError("Cannot specify both _FilePath and _FileBytes")

if self.Id and int(self.Id) > 0:
json_data = qb.update_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)
json_data = qb.update_object(self.qbo_object_name, self.to_json(),
_file_path=self._FilePath,
_file_bytes=self._FileBytes)
else:
json_data = qb.create_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)
json_data = qb.create_object(self.qbo_object_name, self.to_json(),
_file_path=self._FilePath,
_file_bytes=self._FileBytes)

if self.Id is None and self.FileName:
obj = type(self).from_json(json_data['AttachableResponse'][0]['Attachable'])
Expand Down
6 changes: 3 additions & 3 deletions quickbooks/objects/employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Employee(QuickbooksManagedObject, QuickbooksTransactionEntity):

def __init__(self):
super(Employee, self).__init__()
self.SSN = ""
self.SSN = None

self.GivenName = ""
self.FamilyName = ""
Expand All @@ -29,9 +29,9 @@ def __init__(self):
self.Title = ""
self.BillRate = 0
self.CostRate = 0
self.BirthDate = ""
self.BirthDate = None
self.Gender = None
self.HiredDate = ""
self.HiredDate = None
self.ReleasedDate = ""
self.Active = True
self.Organization = False
Expand Down
1 change: 1 addition & 0 deletions quickbooks/objects/timeactivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self):
self.StartTime = None
self.EndTime = None
self.Description = None
self.CostRate = None

self.VendorRef = None
self.CustomerRef = None
Expand Down
7 changes: 2 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
[metadata]
description-file = README.md

[flake8]
max-line-length = 100
max-complexity = 10
max_line_length = 100
max_complexity = 10
filename = *.py
format = default
exclude =/quickbooks/objects/__init__.py
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def read(*parts):
return fp.read()


VERSION = (0, 9, 11)
VERSION = (0, 9, 12)
version = '.'.join(map(str, VERSION))

setup(
Expand All @@ -30,7 +30,6 @@ def read(*parts):
},

install_requires=[
'setuptools',
'intuit-oauth==1.2.6',
'requests_oauthlib>=1.3.1',
'requests>=2.31.0',
Expand Down
Loading