diff --git a/README.md b/README.md index 68067d53..414d9957 100644 --- a/README.md +++ b/README.md @@ -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 you’re 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). @@ -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. diff --git a/quickbooks/client.py b/quickbooks/client.py index d83a62d0..31c1a94c 100644 --- a/quickbooks/client.py +++ b/quickbooks/client.py @@ -152,7 +152,7 @@ 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 = {} @@ -172,7 +172,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({ @@ -183,8 +183,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'] @@ -287,11 +290,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 @@ -307,9 +310,9 @@ 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) + result = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params) return result diff --git a/quickbooks/objects/attachable.py b/quickbooks/objects/attachable.py index 23e71321..ccb22609 100644 --- a/quickbooks/objects/attachable.py +++ b/quickbooks/objects/attachable.py @@ -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 @@ -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']) diff --git a/tests/integration/test_attachable.py b/tests/integration/test_attachable.py index 2eeffabd..2639bb10 100644 --- a/tests/integration/test_attachable.py +++ b/tests/integration/test_attachable.py @@ -68,4 +68,26 @@ def test_create_file(self): self.assertEqual(query_attachable.AttachableRef[0].EntityRef.value, vendor.Id) self.assertEqual(query_attachable.Note, "Sample note") + def test_create_file_from_bytes(self): + attachable = Attachable() + file_content = b"File contents in bytes" + + vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] + + attachable_ref = AttachableRef() + attachable_ref.EntityRef = vendor.to_ref() + attachable.AttachableRef.append(attachable_ref) + + attachable.Note = "Sample note with bytes" + attachable.FileName = "test.txt" + attachable._FileBytes = file_content + attachable.ContentType = 'text/plain' + + attachable.save(qb=self.qb_client) + + query_attachable = Attachable.get(attachable.Id, qb=self.qb_client) + + self.assertEqual(query_attachable.AttachableRef[0].EntityRef.value, vendor.Id) + self.assertEqual(query_attachable.Note, "Sample note with bytes") + diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 23c20632..a1acad2e 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -128,7 +128,7 @@ def test_update_object_with_request_id(self, make_req): qb_client.update_object("Customer", "request_body", request_id="123") url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/customer" - make_req.assert_called_with("POST", url, "request_body", file_path=None, params=None, request_id="123") + make_req.assert_called_with("POST", url, "request_body", file_path=None, file_bytes=None, request_id="123", params=None) @patch('quickbooks.client.QuickBooks.get') def test_get_current_user(self, get):