Skip to content

Commit 04d5563

Browse files
authored
Merge branch '0.9.12' into minor-version
2 parents b4014b4 + 5efc535 commit 04d5563

20 files changed

+224
-86
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 1 deletion
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

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

README.md

Lines changed: 17 additions & 1 deletion
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def change_data_capture(self, entity_string, changed_since):
157157
return result
158158

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

162162
if not params:
163163
params = {}
@@ -176,7 +176,7 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
176176
'User-Agent': 'python-quickbooks V3 library'
177177
}
178178

179-
if file_path:
179+
if file_path or file_bytes:
180180
url = url.replace('attachable', 'upload')
181181
boundary = '-------------PythonMultipartPost'
182182
headers.update({
@@ -187,8 +187,11 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
187187
'Connection': 'close'
188188
})
189189

190-
with open(file_path, 'rb') as attachment:
191-
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'))
192195

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

@@ -260,9 +263,10 @@ def process_request(self, request_type, url, headers="", params="", data=""):
260263
request_type, url, headers=headers, params=params, data=data)
261264

262265
def get_single_object(self, qbbo, pk, params=None):
263-
url = "{0}/company/{1}/{2}/{3}/".format(self.api_url, self.company_id, qbbo.lower(), pk)
266+
url = "{0}/company/{1}/{2}/{3}".format(self.api_url, self.company_id, qbbo.lower(), pk)
264267
if params is None:
265268
params = {}
269+
266270
return self.get(url, {}, params=params)
267271

268272
@staticmethod
@@ -299,11 +303,11 @@ def handle_exceptions(results):
299303
else:
300304
raise exceptions.QuickbooksException(message, code, detail)
301305

302-
def create_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
306+
def create_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
303307
self.isvalid_object_name(qbbo)
304308

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

308312
return results
309313

@@ -319,11 +323,14 @@ def isvalid_object_name(self, object_name):
319323

320324
return True
321325

322-
def update_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
326+
def update_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
323327
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
324328
if params is None:
325329
params = {}
326-
return self.post(url, request_body, file_path=_file_path, request_id=request_id, params=params)
330+
331+
result = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params)
332+
333+
return result
327334

328335
def delete_object(self, qbbo, request_body, _file_path=None, request_id=None):
329336
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())

quickbooks/mixins.py

Lines changed: 20 additions & 8 deletions
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

Lines changed: 11 additions & 2 deletions
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/timeactivity.py

Lines changed: 1 addition & 0 deletions
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.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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',

tests/integration/test_account.py

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,77 @@ def setUp(self):
1313

1414
def test_create(self):
1515
account = Account()
16-
account.AcctNum = self.account_number
17-
account.Name = self.name
16+
# Use shorter timestamp for uniqueness (within 20 char limit)
17+
timestamp = datetime.now().strftime('%m%d%H%M%S')
18+
unique_number = f"T{timestamp}" # T for Test
19+
unique_name = f"Test Account {timestamp}"
20+
21+
account.AcctNum = unique_number
22+
account.Name = unique_name
23+
account.AccountType = "Bank" # Required field
1824
account.AccountSubType = "CashOnHand"
19-
account.save(qb=self.qb_client)
2025

21-
self.id = account.Id
22-
query_account = Account.get(account.Id, qb=self.qb_client)
26+
created_account = account.save(qb=self.qb_client)
27+
28+
# Verify the save was successful
29+
self.assertIsNotNone(created_account)
30+
self.assertIsNotNone(created_account.Id)
31+
self.assertTrue(int(created_account.Id) > 0)
2332

24-
self.assertEqual(account.Id, query_account.Id)
25-
self.assertEqual(query_account.Name, self.name)
26-
self.assertEqual(query_account.AcctNum, self.account_number)
33+
query_account = Account.get(created_account.Id, qb=self.qb_client)
34+
35+
self.assertEqual(created_account.Id, query_account.Id)
36+
self.assertEqual(query_account.Name, unique_name)
37+
self.assertEqual(query_account.AcctNum, unique_number)
38+
self.assertEqual(query_account.AccountType, "Bank")
39+
self.assertEqual(query_account.AccountSubType, "CashOnHand")
2740

2841
def test_update(self):
29-
account = Account.filter(Name=self.name, qb=self.qb_client)[0]
42+
# First create an account with a unique name and number
43+
timestamp = datetime.now().strftime('%m%d%H%M%S')
44+
unique_number = f"T{timestamp}"
45+
unique_name = f"Test Account {timestamp}"
46+
47+
account = Account()
48+
account.AcctNum = unique_number
49+
account.Name = unique_name
50+
account.AccountType = "Bank"
51+
account.AccountSubType = "CashOnHand"
3052

31-
account.Name = "Updated Name {0}".format(self.account_number)
32-
account.save(qb=self.qb_client)
53+
created_account = account.save(qb=self.qb_client)
54+
55+
# Verify the save was successful
56+
self.assertIsNotNone(created_account)
57+
self.assertIsNotNone(created_account.Id)
3358

34-
query_account = Account.get(account.Id, qb=self.qb_client)
35-
self.assertEqual(query_account.Name, "Updated Name {0}".format(self.account_number))
59+
# Change the name
60+
updated_name = f"{unique_name}_updated"
61+
created_account.Name = updated_name
62+
updated_account = created_account.save(qb=self.qb_client)
63+
64+
# Query the account and make sure it has changed
65+
query_account = Account.get(updated_account.Id, qb=self.qb_client)
66+
self.assertEqual(query_account.Name, updated_name)
67+
self.assertEqual(query_account.AcctNum, unique_number) # Account number should not change
3668

3769
def test_create_using_from_json(self):
70+
timestamp = datetime.now().strftime('%m%d%H%M%S')
71+
unique_number = f"T{timestamp}"
72+
unique_name = f"Test JSON {timestamp}"
73+
3874
account = Account.from_json({
39-
"AcctNum": datetime.now().strftime('%d%H%M%S'),
40-
"Name": "{} {}".format(self.name, self.time.strftime("%Y-%m-%d %H:%M:%S")),
75+
"AcctNum": unique_number,
76+
"Name": unique_name,
77+
"AccountType": "Bank",
4178
"AccountSubType": "CashOnHand"
4279
})
4380

44-
account.save(qb=self.qb_client)
81+
created_account = account.save(qb=self.qb_client)
82+
self.assertIsNotNone(created_account)
83+
self.assertIsNotNone(created_account.Id)
84+
85+
# Verify we can get the account
86+
query_account = Account.get(created_account.Id, qb=self.qb_client)
87+
self.assertEqual(query_account.Name, unique_name)
88+
self.assertEqual(query_account.AccountType, "Bank")
89+
self.assertEqual(query_account.AccountSubType, "CashOnHand")

0 commit comments

Comments
 (0)