diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f8b25077..10f03ced 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/Makefile b/Makefile index c59408cb..d12b0bb9 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + find . -name '*.tgz' -delete diff --git a/Pipfile b/Pipfile index 31d70a4f..e8e31374 100644 --- a/Pipfile +++ b/Pipfile @@ -14,4 +14,4 @@ urllib3 = ">=2.1.0" intuit-oauth = "==1.2.6" requests = ">=2.31.0" requests_oauthlib = ">=1.3.1" -setuptools = "*" +build = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 1f088af2..5cd34380 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ac47337a60c459de39be3440865879adc5be2217a477fbec6374e69659297513" + "sha256": "dd51fe531ba0bc1e934a0b630dc0b9b838cde70d44207667346ba4701233ba86" }, "pipfile-spec": 6, "requires": {}, @@ -14,6 +14,15 @@ ] }, "default": { + "build": { + "hashes": [ + "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", + "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.2.2.post1" + }, "certifi": { "hashes": [ "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", @@ -254,6 +263,14 @@ "markers": "python_version >= '3.6'", "version": "==3.2.2" }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, "pycparser": { "hashes": [ "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", @@ -273,6 +290,14 @@ "markers": "python_version >= '3.8'", "version": "==2.9.0" }, + "pyproject-hooks": { + "hashes": [ + "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", + "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", @@ -290,15 +315,6 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "setuptools": { - "hashes": [ - "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1", - "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==72.1.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 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/dev_requirements.txt b/dev_requirements.txt index 74c3c14b..05c1b035 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,3 @@ coverage==7.3.0 ipdb==0.13.13 -mock==5.1.0 nose==1.3.7 \ No newline at end of file diff --git a/quickbooks/client.py b/quickbooks/client.py index d83a62d0..a4280444 100644 --- a/quickbooks/client.py +++ b/quickbooks/client.py @@ -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) @@ -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 @@ -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({ @@ -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'] @@ -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: @@ -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): @@ -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 @@ -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 diff --git a/quickbooks/mixins.py b/quickbooks/mixins.py index 49db76fa..60140599 100644 --- a/quickbooks/mixins.py +++ b/quickbooks/mixins.py @@ -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): 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/quickbooks/objects/employee.py b/quickbooks/objects/employee.py index 831ef86f..64d4e0ff 100644 --- a/quickbooks/objects/employee.py +++ b/quickbooks/objects/employee.py @@ -17,7 +17,7 @@ class Employee(QuickbooksManagedObject, QuickbooksTransactionEntity): def __init__(self): super(Employee, self).__init__() - self.SSN = "" + self.SSN = None self.GivenName = "" self.FamilyName = "" @@ -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 diff --git a/quickbooks/objects/timeactivity.py b/quickbooks/objects/timeactivity.py index feea8887..1d4bde48 100644 --- a/quickbooks/objects/timeactivity.py +++ b/quickbooks/objects/timeactivity.py @@ -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 diff --git a/setup.cfg b/setup.cfg index 4f71077b..29ffc5c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/setup.py b/setup.py index 6198d5f6..b9d6fb9c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def read(*parts): return fp.read() -VERSION = (0, 9, 11) +VERSION = (0, 9, 12) version = '.'.join(map(str, VERSION)) setup( @@ -30,7 +30,6 @@ def read(*parts): }, install_requires=[ - 'setuptools', 'intuit-oauth==1.2.6', 'requests_oauthlib>=1.3.1', 'requests>=2.31.0', diff --git a/tests/integration/test_account.py b/tests/integration/test_account.py index b90c953d..741a6b7a 100644 --- a/tests/integration/test_account.py +++ b/tests/integration/test_account.py @@ -13,32 +13,77 @@ def setUp(self): def test_create(self): account = Account() - account.AcctNum = self.account_number - account.Name = self.name + # Use shorter timestamp for uniqueness (within 20 char limit) + timestamp = datetime.now().strftime('%m%d%H%M%S') + unique_number = f"T{timestamp}" # T for Test + unique_name = f"Test Account {timestamp}" + + account.AcctNum = unique_number + account.Name = unique_name + account.AccountType = "Bank" # Required field account.AccountSubType = "CashOnHand" - account.save(qb=self.qb_client) - self.id = account.Id - query_account = Account.get(account.Id, qb=self.qb_client) + created_account = account.save(qb=self.qb_client) - self.assertEqual(account.Id, query_account.Id) - self.assertEqual(query_account.Name, self.name) - self.assertEqual(query_account.AcctNum, self.account_number) + # Verify the save was successful + self.assertIsNotNone(created_account) + self.assertIsNotNone(created_account.Id) + self.assertTrue(int(created_account.Id) > 0) + + query_account = Account.get(created_account.Id, qb=self.qb_client) + + self.assertEqual(created_account.Id, query_account.Id) + self.assertEqual(query_account.Name, unique_name) + self.assertEqual(query_account.AcctNum, unique_number) + self.assertEqual(query_account.AccountType, "Bank") + self.assertEqual(query_account.AccountSubType, "CashOnHand") def test_update(self): - account = Account.filter(Name=self.name, qb=self.qb_client)[0] + # First create an account with a unique name and number + timestamp = datetime.now().strftime('%m%d%H%M%S') + unique_number = f"T{timestamp}" + unique_name = f"Test Account {timestamp}" + + account = Account() + account.AcctNum = unique_number + account.Name = unique_name + account.AccountType = "Bank" + account.AccountSubType = "CashOnHand" + + created_account = account.save(qb=self.qb_client) + + # Verify the save was successful + self.assertIsNotNone(created_account) + self.assertIsNotNone(created_account.Id) - account.Name = "Updated Name {0}".format(self.account_number) - account.save(qb=self.qb_client) + # Change the name + updated_name = f"{unique_name}_updated" + created_account.Name = updated_name + updated_account = created_account.save(qb=self.qb_client) - query_account = Account.get(account.Id, qb=self.qb_client) - self.assertEqual(query_account.Name, "Updated Name {0}".format(self.account_number)) + # Query the account and make sure it has changed + query_account = Account.get(updated_account.Id, qb=self.qb_client) + self.assertEqual(query_account.Name, updated_name) + self.assertEqual(query_account.AcctNum, unique_number) # Account number should not change def test_create_using_from_json(self): + timestamp = datetime.now().strftime('%m%d%H%M%S') + unique_number = f"T{timestamp}" + unique_name = f"Test JSON {timestamp}" + account = Account.from_json({ - "AcctNum": datetime.now().strftime('%d%H%M%S'), - "Name": "{} {}".format(self.name, self.time.strftime("%Y-%m-%d %H:%M:%S")), + "AcctNum": unique_number, + "Name": unique_name, + "AccountType": "Bank", "AccountSubType": "CashOnHand" }) - account.save(qb=self.qb_client) + created_account = account.save(qb=self.qb_client) + self.assertIsNotNone(created_account) + self.assertIsNotNone(created_account.Id) + + # Verify we can get the account + query_account = Account.get(created_account.Id, qb=self.qb_client) + self.assertEqual(query_account.Name, unique_name) + self.assertEqual(query_account.AccountType, "Bank") + self.assertEqual(query_account.AccountSubType, "CashOnHand") 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/integration/test_base.py b/tests/integration/test_base.py index 404d6a4f..aca87d42 100644 --- a/tests/integration/test_base.py +++ b/tests/integration/test_base.py @@ -17,10 +17,10 @@ def setUp(self): ) self.qb_client = QuickBooks( - minorversion=73, auth_client=self.auth_client, refresh_token=os.environ.get('REFRESH_TOKEN'), company_id=os.environ.get('COMPANY_ID'), + minorversion=75 ) self.qb_client.sandbox = True @@ -41,6 +41,7 @@ def setUp(self): # auth_client=self.auth_client, refresh_token='REFRESH_TOKEN', company_id='COMPANY_ID', + minorversion=75 ) self.qb_client.sandbox = True diff --git a/tests/integration/test_employee.py b/tests/integration/test_employee.py index 3d8c2235..2f389130 100644 --- a/tests/integration/test_employee.py +++ b/tests/integration/test_employee.py @@ -1,15 +1,17 @@ -from datetime import datetime +from datetime import datetime, date from quickbooks.objects.base import Address, PhoneNumber from quickbooks.objects.employee import Employee from tests.integration.test_base import QuickbooksTestCase +from quickbooks.helpers import qb_date_format class EmployeeTest(QuickbooksTestCase): def test_create(self): employee = Employee() - employee.SSN = "444-55-6666" + employee.SSN = None employee.GivenName = "John" + employee.HiredDate = qb_date_format(date(2020, 7, 22)) employee.FamilyName = "Smith {0}".format(datetime.now().strftime('%d%H%M%S')) employee.PrimaryAddr = Address() @@ -19,13 +21,13 @@ def test_create(self): employee.PrimaryAddr.PostalCode = "93242" employee.PrimaryPhone = PhoneNumber() - employee.PrimaryPhone.FreeFormNumber = "408-525-1234" + employee.PrimaryPhone.FreeFormNumber = "4085251234" + employee.save(qb=self.qb_client) query_employee = Employee.get(employee.Id, qb=self.qb_client) self.assertEqual(query_employee.Id, employee.Id) - self.assertEqual(query_employee.SSN, "XXX-XX-XXXX") self.assertEqual(query_employee.GivenName, employee.GivenName) self.assertEqual(query_employee.FamilyName, employee.FamilyName) self.assertEqual(query_employee.PrimaryAddr.Line1, employee.PrimaryAddr.Line1) diff --git a/tests/integration/test_item.py b/tests/integration/test_item.py index ee12c686..f8ae4dcd 100644 --- a/tests/integration/test_item.py +++ b/tests/integration/test_item.py @@ -45,3 +45,22 @@ def test_create(self): self.assertEqual(query_item.IncomeAccountRef.value, self.income_account.Id) self.assertEqual(query_item.ExpenseAccountRef.value, self.expense_account.Id) self.assertEqual(query_item.AssetAccountRef.value, self.asset_account.Id) + + def test_sku_in_all(self): + """Test that SKU is properly returned when using Item.all()""" + # First create an item with a SKU + unique_name = "Test SKU Item {0}".format(datetime.now().strftime('%d%H%M%S')) + item = Item() + item.Name = unique_name + item.Type = "Service" + item.Sku = "TEST_SKU_" + self.account_number + item.IncomeAccountRef = self.income_account.to_ref() + item.ExpenseAccountRef = self.expense_account.to_ref() + item.save(qb=self.qb_client) + + # Now fetch all items and verify the SKU is present + items = Item.all(max_results=100, qb=self.qb_client) + found_item = next((i for i in items if i.Id == item.Id), None) + + self.assertIsNotNone(found_item, "Created item not found in Item.all() results") + self.assertEqual(found_item.Sku, "TEST_SKU_" + self.account_number) diff --git a/tests/integration/test_timeactivity.py b/tests/integration/test_timeactivity.py index 707d65de..39ad1a0b 100644 --- a/tests/integration/test_timeactivity.py +++ b/tests/integration/test_timeactivity.py @@ -22,6 +22,7 @@ def test_create(self): time_activity.Description = "Test description" time_activity.StartTime = qb_datetime_utc_offset_format(datetime(2016, 7, 22, 10, 0), '-07:00') time_activity.EndTime = qb_datetime_utc_offset_format(datetime(2016, 7, 22, 11, 0), '-07:00') + time_activity.CostRate = 50.0 time_activity.save(qb=self.qb_client) query_time_activity = TimeActivity.get(time_activity.Id, qb=self.qb_client) @@ -30,6 +31,7 @@ def test_create(self): self.assertEqual(query_time_activity.NameOf, "Employee") self.assertEqual(query_time_activity.Description, "Test description") self.assertEqual(query_time_activity.EmployeeRef.value, employee.Id) + self.assertEqual(query_time_activity.CostRate, 50.0) # Quickbooks has issues with returning the correct StartTime and EndTime #self.assertEqual(query_time_activity.StartTime, '2016-07-22T10:00:00-07:00') @@ -38,8 +40,10 @@ def test_create(self): def test_update(self): time_activity = TimeActivity.all(max_results=1, qb=self.qb_client)[0] time_activity.Description = "Updated test description" + time_activity.CostRate = 75.0 time_activity.save(qb=self.qb_client) query_time_activity = TimeActivity.get(time_activity.Id, qb=self.qb_client) self.assertEqual(query_time_activity.Description, "Updated test description") + self.assertEqual(query_time_activity.CostRate, 75.0) diff --git a/tests/unit/objects/test_timeactivity.py b/tests/unit/objects/test_timeactivity.py index fa9f870b..586f3d1d 100644 --- a/tests/unit/objects/test_timeactivity.py +++ b/tests/unit/objects/test_timeactivity.py @@ -9,7 +9,6 @@ def test_unicode(self): time_activity = TimeActivity() time_activity.NameOf = "test" - time_activity.TimeZone = "CST" time_activity.BillableStatus = "test" time_activity.Taxable = False time_activity.HourlyRate = 0 @@ -18,9 +17,9 @@ def test_unicode(self): time_activity.BreakHours = 1 time_activity.BreakMinutes = 60 time_activity.Description = "test" + time_activity.CostRate = 50.0 self.assertEqual(str(time_activity), "test") - self.assertEqual(time_activity.TimeZone, "CST") self.assertEqual(time_activity.BillableStatus, "test") self.assertEqual(time_activity.Taxable, False) self.assertEqual(time_activity.HourlyRate, 0) @@ -29,6 +28,7 @@ def test_unicode(self): self.assertEqual(time_activity.BreakHours, 1) self.assertEqual(time_activity.BreakMinutes, 60) self.assertEqual(time_activity.Description, "test") + self.assertEqual(time_activity.CostRate, 50.0) def test_valid_object_name(self): obj = TimeActivity() diff --git a/tests/unit/test_batch.py b/tests/unit/test_batch.py index e3bacd8f..9bc71f74 100644 --- a/tests/unit/test_batch.py +++ b/tests/unit/test_batch.py @@ -1,8 +1,5 @@ import unittest -try: - from mock import patch -except ImportError: - from unittest.mock import patch +from unittest.mock import patch from quickbooks import batch, client from quickbooks.objects.customer import Customer from quickbooks.exceptions import QuickbooksException diff --git a/tests/unit/test_cdc.py b/tests/unit/test_cdc.py index d115177a..da9ae0be 100644 --- a/tests/unit/test_cdc.py +++ b/tests/unit/test_cdc.py @@ -1,8 +1,5 @@ import unittest -try: - from mock import patch -except ImportError: - from unittest.mock import patch +from unittest.mock import patch from quickbooks.cdc import change_data_capture from quickbooks.objects import Invoice, Customer from quickbooks import QuickBooks diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 23c20632..94b1eff7 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,11 +1,7 @@ import json import warnings from tests.integration.test_base import QuickbooksUnitTestCase - -try: - from mock import patch, mock_open -except ImportError: - from unittest.mock import patch, mock_open +from unittest.mock import patch, mock_open from quickbooks.exceptions import QuickbooksException, SevereException, AuthorizationException from quickbooks import client, mixins @@ -32,12 +28,10 @@ def test_client_new(self): self.qb_client = client.QuickBooks( company_id="company_id", verbose=True, - minorversion=75, verifier_token=TEST_VERIFIER_TOKEN, ) self.assertEqual(self.qb_client.company_id, "company_id") - self.assertEqual(self.qb_client.minorversion, 75) def test_client_with_deprecated_minor_version(self): with warnings.catch_warnings(record=True) as w: @@ -53,7 +47,8 @@ def test_client_with_deprecated_minor_version(self): self.assertEqual(self.qb_client.minorversion, 74) self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertTrue("Minor Version no longer supported." in str(w[-1].message)) + self.assertTrue("Minor Version 74 is no longer supported" in str(w[-1].message)) + self.assertTrue("Minimum supported version is 75" in str(w[-1].message)) def test_api_url(self): qb_client = client.QuickBooks(sandbox=False) @@ -128,7 +123,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={}) @patch('quickbooks.client.QuickBooks.get') def test_get_current_user(self, get): @@ -146,7 +141,8 @@ def test_get_report(self, make_req): qb_client.get_report("profitandloss", {1: 2}) url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/reports/profitandloss" - make_req.assert_called_with("GET", url, params={1: 2}) + expected_params = {1: 2} + make_req.assert_called_with("GET", url, params=expected_params) @patch('quickbooks.client.QuickBooks.make_request') def test_get_single_object(self, make_req): @@ -154,8 +150,8 @@ def test_get_single_object(self, make_req): qb_client.company_id = "1234" qb_client.get_single_object("test", 1) - url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/test/1/" - make_req.assert_called_with("GET", url, {}, params=None) + url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/test/1" + make_req.assert_called_with("GET", url, {}, params={}) @patch('quickbooks.client.QuickBooks.make_request') def test_get_single_object_with_params(self, make_req): @@ -163,7 +159,7 @@ def test_get_single_object_with_params(self, make_req): qb_client.company_id = "1234" qb_client.get_single_object("test", 1, params={'param':'value'}) - url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/test/1/" + url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/test/1" make_req.assert_called_with("GET", url, {}, params={'param':'value'}) @patch('quickbooks.client.QuickBooks.process_request') @@ -177,7 +173,8 @@ def test_make_request(self, process_request): process_request.assert_called_with( "GET", url, data={}, - headers={'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'python-quickbooks V3 library'}, params={}) + headers={'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'python-quickbooks V3 library'}, + params={'minorversion': client.QuickBooks.MINIMUM_MINOR_VERSION}) def test_handle_exceptions(self): qb_client = client.QuickBooks() @@ -264,7 +261,7 @@ def test_make_request_file_closed(self, process_request): class MockResponse(object): @property def text(self): - return "oauth_token_secret=secretvalue&oauth_callback_confirmed=true&oauth_token=tokenvalue" + return '{"QueryResponse": {"Department": []}}' @property def status_code(self): @@ -275,10 +272,8 @@ def status_code(self): return httplib.OK def json(self): - return "{}" + return json.loads(self.text) - def content(self): - return '' class MockResponseJson: def __init__(self, json_data=None, status_code=200): @@ -327,5 +322,8 @@ def get_session(self): class MockSession(object): - def request(self, request_type, url, no_idea, company_id, **kwargs): + def __init__(self): + self.access_token = "test_access_token" + + def request(self, request_type, url, headers=None, params=None, data=None, **kwargs): return MockResponse() diff --git a/tests/unit/test_mixins.py b/tests/unit/test_mixins.py index 0c413bf8..d2d6fc31 100644 --- a/tests/unit/test_mixins.py +++ b/tests/unit/test_mixins.py @@ -1,14 +1,13 @@ import unittest from urllib.parse import quote +from unittest import TestCase +from datetime import datetime +from unittest.mock import patch, ANY from quickbooks.objects import Bill, Invoice, Payment, BillPayment from tests.integration.test_base import QuickbooksUnitTestCase - -try: - from mock import patch -except ImportError: - from unittest.mock import patch +from tests.unit.test_client import MockSession from quickbooks.objects.base import PhoneNumber, QuickbooksBaseObject from quickbooks.objects.department import Department @@ -130,15 +129,17 @@ def test_to_dict(self): class ListMixinTest(QuickbooksUnitTestCase): - @patch('quickbooks.mixins.ListMixin.where') - def test_all(self, where): + @patch('quickbooks.mixins.ListMixin.query') + def test_all(self, query): + query.return_value = [] Department.all() - where.assert_called_once_with('', order_by='', max_results=100, start_position='', qb=None) + query.assert_called_once_with("SELECT * FROM Department MAXRESULTS 100", qb=ANY) def test_all_with_qb(self): + self.qb_client.session = MockSession() # Add a mock session with patch.object(self.qb_client, 'query') as query: Department.all(qb=self.qb_client) - self.assertTrue(query.called) + query.assert_called_once() @patch('quickbooks.mixins.ListMixin.where') def test_filter(self, where):