diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..d76c6a23 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,23 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000..804cd82c --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest coverage pytest-cov + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest tests/unit --cov + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fb9de727..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -python: - - 3.5 - - 3.6 - - 3.7 - - 3.8 -before_install: - - pip install coveralls -install: - - pip install -e . - -script: nosetests -w tests/unit --with-coverage --cover-package=quickbooks -after_success: - - coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 43ee924e..10f03ced 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,95 @@ 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 + +* 0.9.10 (August 7, 2024) + * Update intuit-oauth dependency + * Fix issues with Invoice Sharable Link + * Added optional params to get + +* 0.9.9 (July 9, 2024) + * Removed simplejson + * Added use_decimal option (See PR: https://github.com/ej2/python-quickbooks/pull/356 for details) + +* 0.9.8 (May 20, 2024) + * Added ItemAccountRef to SalesItemLineDetail + * Updated from_json example in readme + +* 0.9.7 (March 12, 2024) + * Update intuit-oauth dependency + * Updated CompanyCurrency to ref to use Code instead of Id + * Added missing CurrentRef property from customer object + * Made improvements to file attachment handling + +* 0.9.6 (January 2, 2024) + * Replace RAuth with requests_oauthlib + * Removed python 2 code from client.py + * Removed unused dependencies from Pipfile + * Added new fields to Employee object + * Added VendorAddr to Bill object + * Added new fields to Estimate object + * Fix TaxInclusiveAmt and vendor setting 1099 creation + * Updated readme and contributing + +* 0.9.5 (November 1, 2023) + * Added the ability to void all voidable QB types + * Added to_ref to CreditMemo object + * Added ProjectRef and ShipFromAddr to Estimate + * Added missing initialization for objects on DiscountLineDetail, Estimate, Employee, and Invoice + +* 0.9.4 (August 29, 2023) + * Removed python 2 compatible decorators + * Removed python 2 dependencies + * Fixed issue with MarkupInfo field on AccountBasedExpenseLineDetail + * Removed test files from package + +* 0.9.3 (March 7, 2023) + * Added support for Recurring Transaction + * Added support for optional query params + * Fixed errors in example code on the readme + * Removed enable_global and disable_global + +* 0.9.2 (August 3, 2022) + * Removed pycparser dependency + * Added new fields to CreditCardPayment object + * Added new fields to Invoice object + * Added new fields to Payment object + * Added to_linked_txn method to Payment object + * Added new object CustomerType + * Added MetaData to CompanyInfo + * Added update support to CompanyInfo + * Added new fields to Preferences object + * Improved exception object + +* 0.9.1 (November 30, 2021) + * Added response status code when raising unauthorized exceptions + * Added pending deprecation warnings to enable_global and disable_global + * Added more detailed messages in raised exceptions + * Added void method to Payment object + * Added option for invoice link + * Added support for idempotent behavior using Request ID parameter + +* 0.9.0 (July 20, 2021) + * Added missing TxnDate to Invoice + * Updated requirements + * Added BillRate to Vendor + * Added IsProject to Customer + * Added Refresh Token to Client Instance + * Updated Estimate and CreditMemo to use DescriptionOnlyLine + * Removed unused DescriptionLine object + * Added support for Preferences entity + * Added support for ExchangeRate entity + * 0.8.4 (October 11, 2020) * Added support for the CreditCardPayment entity * Updated readme diff --git a/MANIFEST.in b/MANIFEST.in index d49e8c4c..9a8b7c4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.md include LICENSE -recursive-include tests * \ No newline at end of file +recursive-exclude tests * \ No newline at end of file diff --git a/Makefile b/Makefile index 420f0b80..d12b0bb9 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +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 eb9fdc15..b0ea8afa 100644 --- a/Pipfile +++ b/Pipfile @@ -4,17 +4,15 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] - -[packages] -intuit-oauth = "==1.2.3" -rauth = ">=0.7.1" -requests = ">=2.19.1" -simplejson = ">=3.17.0" -six = ">=1.14.0" -nose = "*" -authclient = "*" coverage = "*" twine = "*" +pytest = "*" +pytest-cov = "*" -[requires] -python_version = "3.8" +[packages] +urllib3 = ">=2.1.0" +intuit-oauth = "==1.2.6" +requests = ">=2.31.0" +requests_oauthlib = ">=1.3.1" +build = "*" +twine = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 2b562e36..220836a7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "19d2381be21fa243498ecda6cacf06ad6ba0784c03f7f71afbb208858efd045b" + "sha256": "37e1068d8771427de42f27a0fe22a2e43995ba6c50304dfc3595fe3a6185b33e" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -16,434 +14,884 @@ ] }, "default": { - "aiohttp": { + "build": { "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", + "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.6.2" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.2.2.post1" }, - "async-timeout": { + "certifi": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.0.1" + "markers": "python_version >= '3.6'", + "version": "==2025.1.31" + }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" + }, + "cryptography": { + "hashes": [ + "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", + "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", + "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", + "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", + "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", + "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", + "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", + "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", + "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", + "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", + "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", + "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", + "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", + "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", + "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", + "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", + "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", + "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", + "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", + "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", + "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", + "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", + "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", + "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", + "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", + "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", + "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", + "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", + "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", + "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", + "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", + "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", + "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", + "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", + "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308" + ], + "version": "==44.0.2" }, - "attrs": { + "docutils": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "markers": "python_version >= '3.9'", + "version": "==0.21.2" + }, + "enum-compat": { + "hashes": [ + "sha256:3677daabed56a6f724451d585662253d8fb4e5569845aafa8bb0da36b1a8751e", + "sha256:88091b617c7fc3bbbceae50db5958023c48dc40b50520005aa3bf27f8f7ea157" + ], + "version": "==0.0.3" }, - "authclient": { + "id": { "hashes": [ - "sha256:77fe31eb7b8b7c8cf947fc449ef06d48576bae675299ea612c508c2f7eae9d9f", - "sha256:f56a63107c7cf26a84274ba6f10eae509d76fa5f391fee0a80af44d2cea00481" + "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", + "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "intuit-oauth": { + "hashes": [ + "sha256:4c25b3fcbdb5aaaa65dcc8f0f71e8f8400dcaa4dcdac58b8333d5f1b11a8f82d", + "sha256:b93439e8135d536acdbe53cf9842930ade2205410c6ab3530fb1dbea12eee5d0" ], "index": "pypi", - "version": "==1.0" + "version": "==1.2.6" }, - "bleach": { + "jaraco.classes": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1.5" + "markers": "python_version >= '3.8'", + "version": "==3.4.0" }, - "certifi": { + "jaraco.context": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" ], - "version": "==2020.6.20" + "markers": "python_version >= '3.8'", + "version": "==6.0.1" }, - "chardet": { + "jaraco.functools": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", + "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649" ], - "version": "==3.0.4" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, - "colorama": { + "keyring": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.3" + "markers": "platform_machine != 'ppc64le' and platform_machine != 's390x'", + "version": "==25.6.0" }, - "coverage": { + "markdown-it-py": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", + "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89" + ], + "markers": "python_version >= '3.9'", + "version": "==10.6.0" + }, + "nh3": { + "hashes": [ + "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", + "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", + "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", + "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", + "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", + "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", + "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", + "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", + "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", + "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", + "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", + "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", + "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", + "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", + "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", + "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", + "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", + "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", + "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", + "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", + "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", + "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", + "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", + "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.21" + }, + "oauthlib": { + "hashes": [ + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" + ], + "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", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pygments": { + "hashes": [ + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.1" + }, + "pyjwt": { + "extras": [ + "crypto" + ], + "hashes": [ + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" + ], + "markers": "python_version >= '3.9'", + "version": "==2.10.1" + }, + "pyproject-hooks": { + "hashes": [ + "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", + "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "readme-renderer": { + "hashes": [ + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" + ], + "markers": "python_version >= '3.9'", + "version": "==44.0" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", - "version": "==5.2.1" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, - "docutils": { + "requests-oauthlib": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", + "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.16" + "markers": "python_version >= '3.4'", + "version": "==2.0.0" }, - "ecdsa": { + "requests-toolbelt": { "hashes": [ - "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e", - "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe" + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.14.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" }, - "enum-compat": { + "rfc3986": { "hashes": [ - "sha256:3677daabed56a6f724451d585662253d8fb4e5569845aafa8bb0da36b1a8751e", - "sha256:88091b617c7fc3bbbceae50db5958023c48dc40b50520005aa3bf27f8f7ea157" + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" ], - "version": "==0.0.3" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, - "future": { + "rich": { "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", + "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" + "markers": "python_full_version >= '3.8.0'", + "version": "==14.0.0" }, - "idna": { + "six": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "version": "==1.17.0" }, - "intuit-oauth": { + "twine": { "hashes": [ - "sha256:004a83d9904ed5e4ef0582ce39717108351b59f5a26e20283405ba4881738dbf", - "sha256:f245c550f7601174eb55f8bdec50b0e64072fa6b017e2aebb4951e41e5f78b21" + "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", + "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd" ], "index": "pypi", - "version": "==1.2.3" + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, - "keyring": { + "urllib3": { + "hashes": [ + "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", + "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.4.0" + } + }, + "develop": { + "certifi": { "hashes": [ - "sha256:182f94fc0381546489e3e4d90384a8c1d43cc09ffe2eb4a826e7312df6e1be7c", - "sha256:cd4d486803d55bdb13e2d453eb61dbbc984773e4f2b98a455aa85b1f4bc421e4" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==21.3.1" - }, - "multidict": { - "hashes": [ - "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", - "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", - "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", - "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", - "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", - "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", - "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", - "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", - "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", - "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", - "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", - "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", - "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", - "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", - "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", - "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", - "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" - ], - "markers": "python_version >= '3.5'", - "version": "==4.7.6" - }, - "nose": { - "hashes": [ - "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", - "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", - "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + "version": "==2025.1.31" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", + "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", + "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", + "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", + "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", + "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", + "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", + "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", + "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", + "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", + "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", + "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", + "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", + "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", + "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", + "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", + "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", + "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", + "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", + "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", + "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", + "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", + "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", + "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", + "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", + "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", + "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", + "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", + "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", + "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", + "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", + "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", + "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", + "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", + "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", + "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", + "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", + "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", + "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", + "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", + "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", + "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", + "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", + "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", + "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", + "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", + "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", + "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", + "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", + "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", + "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", + "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", + "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", + "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", + "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", + "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", + "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", + "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", + "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", + "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", + "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", + "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", + "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f" ], "index": "pypi", - "version": "==1.3.7" + "markers": "python_version >= '3.9'", + "version": "==7.8.0" }, - "oauthlib": { + "docutils": { "hashes": [ - "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", - "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==0.21.2" }, - "packaging": { + "id": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", + "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4" + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, - "pkginfo": { + "idna": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "version": "==1.5.0.1" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, - "pyasn1": { + "iniconfig": { "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", + "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" ], - "version": "==0.4.8" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, - "pygments": { + "jaraco.classes": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], - "markers": "python_version >= '3.5'", - "version": "==2.6.1" + "markers": "python_version >= '3.8'", + "version": "==3.4.0" }, - "pyparsing": { + "jaraco.context": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.8'", + "version": "==6.0.1" }, - "python-jose": { + "jaraco.functools": { "hashes": [ - "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b", - "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be" + "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", + "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649" ], - "version": "==3.2.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, - "rauth": { + "keyring": { "hashes": [ - "sha256:524cdbc1c28560eacfc9a9d40c59525eb8d00fdf07fbad86107ea24411477b0a", - "sha256:b18590fbd77bc3d871936bbdb851377d1b0c08e337b219c303f8fc2b5a42ef2d" + "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" ], - "index": "pypi", - "version": "==0.7.3" + "markers": "platform_machine != 'ppc64le' and platform_machine != 's390x'", + "version": "==25.6.0" }, - "readme-renderer": { + "markdown-it-py": { "hashes": [ - "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", - "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "version": "==26.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, - "requests": { + "mdurl": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" ], - "index": "pypi", - "version": "==2.24.0" + "markers": "python_version >= '3.7'", + "version": "==0.1.2" }, - "requests-oauthlib": { + "more-itertools": { "hashes": [ - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", - "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" + "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", + "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89" ], - "version": "==1.3.0" + "markers": "python_version >= '3.9'", + "version": "==10.6.0" }, - "requests-toolbelt": { + "nh3": { "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", + "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", + "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", + "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", + "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", + "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", + "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", + "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", + "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", + "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", + "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", + "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", + "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", + "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", + "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", + "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", + "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", + "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", + "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", + "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", + "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", + "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", + "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", + "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286" ], - "version": "==0.9.1" + "markers": "python_version >= '3.8'", + "version": "==0.2.21" }, - "rfc3986": { + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pygments": { "hashes": [ - "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", - "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" - ], - "version": "==1.4.0" - }, - "rsa": { - "hashes": [ - "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", - "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" - ], - "markers": "python_version >= '3.5' and python_version < '4'", - "version": "==4.6" - }, - "simplejson": { - "hashes": [ - "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667", - "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3", - "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043", - "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb", - "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0", - "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d", - "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8", - "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f", - "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf", - "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748", - "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278", - "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4", - "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a", - "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8", - "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d", - "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971", - "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841", - "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f", - "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b", - "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45", - "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9", - "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6", - "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc", - "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956", - "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d", - "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746", - "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a", - "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0", - "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25", - "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625", - "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995", - "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46", - "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f", - "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a", - "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139", - "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f", - "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da", - "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34", - "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b", - "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94", - "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04", - "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b", - "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396", - "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", - "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.1" + }, + "pytest": { + "hashes": [ + "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", + "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" ], "index": "pypi", - "version": "==3.17.2" + "markers": "python_version >= '3.8'", + "version": "==8.3.5" }, - "six": { + "pytest-cov": { + "hashes": [ + "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", + "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.1.1" + }, + "readme-renderer": { + "hashes": [ + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" + ], + "markers": "python_version >= '3.9'", + "version": "==44.0" + }, + "requests": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", - "version": "==1.15.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, - "tqdm": { + "requests-toolbelt": { "hashes": [ - "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", - "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.48.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", + "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==14.0.0" }, "twine": { "hashes": [ - "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", - "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" + "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", + "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd" ], "index": "pypi", - "version": "==3.2.0" + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.10" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "yarl": { - "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", + "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.4.0" } - }, - "develop": {} + } } diff --git a/README.md b/README.md index 0fb41ca7..414d9957 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ python-quickbooks ================= -[![](https://travis-ci.org/ej2/python-quickbooks.svg?branch=master)](https://travis-ci.org/ej2/python-quickbooks) -[![Coverage Status](https://coveralls.io/repos/github/ej2/python-quickbooks/badge.svg?branch=master)](https://coveralls.io/github/ej2/python-quickbooks?branch=master) +[![Python package](https://github.com/ej2/python-quickbooks/actions/workflows/python-package.yml/badge.svg)](https://github.com/ej2/python-quickbooks/actions/workflows/python-package.yml) +[![codecov](https://codecov.io/gh/ej2/python-quickbooks/graph/badge.svg?token=AKXS2F7wvP)](https://codecov.io/gh/ej2/python-quickbooks) [![](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ej2/python-quickbooks/blob/master/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/python-quickbooks)](https://pypi.org/project/python-quickbooks/) 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). +Installation +------------ + +```bash +pip install python-quickbooks +``` + QuickBooks OAuth ------------------------------------------------ @@ -26,115 +34,115 @@ Accessing the API Set up an AuthClient passing in your `CLIENT_ID` and `CLIENT_SECRET`. -```python -from intuitlib.client import AuthClient + from intuitlib.client import AuthClient -auth_client = AuthClient( - client_id='CLIENT_ID', - client_secret='CLIENT_SECRET', - environment='sandbox', - redirect_uri='http://localhost:8000/callback', -) -``` + auth_client = AuthClient( + client_id='CLIENT_ID', + client_secret='CLIENT_SECRET', + access_token='ACCESS_TOKEN', # If you do not pass this in, the Quickbooks client will call refresh and get a new access token. + environment='sandbox', + redirect_uri='http://localhost:8000/callback', + ) Then create a QuickBooks client object passing in the AuthClient, refresh token, and company id: -```python -from quickbooks import QuickBooks - -client = QuickBooks( - auth_client=auth_client, - refresh_token='REFRESH_TOKEN', - company_id='COMPANY_ID', -) -``` -If you need to access a minor version (See [Minor versions](https://developer.intuit.com/docs/0100_quickbooks_online/0200_dev_guides/accounting/minor_versions) for + from quickbooks import QuickBooks + + client = QuickBooks( + auth_client=auth_client, + refresh_token='REFRESH_TOKEN', + company_id='COMPANY_ID', + ) + +If you need to access a minor version (See [Minor versions](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/minor-versions#working-with-minor-versions) for details) pass in minorversion when setting up the client: -```python -client = QuickBooks( - auth_client=auth_client, - refresh_token='REFRESH_TOKEN', - company_id='COMPANY_ID', - minorversion=4 -) -``` + client = QuickBooks( + auth_client=auth_client, + refresh_token='REFRESH_TOKEN', + company_id='COMPANY_ID', + minorversion=69 + ) + +**Note:** Beginning August 1, 2025, Intuit will be deprecating support for minor versions 1–74. See [Intuit Blog](https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/) for more details Object Operations ----------------- List of objects: -```python -from quickbooks.objects.customer import Customer -customers = Customer.all(qb=client) -``` + + from quickbooks.objects.customer import Customer + customers = Customer.all(qb=client) **Note:** The maximum number of entities that can be returned in a response is 1000. If the result size is not specified, the default -number is 100. (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for details) +number is 100. (See [Query operations and syntax](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/data-queries) for details) + +**Warning:** You should never allow user input to pass into a query without sanitizing it first! This library DOES NOT sanitize user input! Filtered list of objects: -```python -customers = Customer.filter(Active=True, FamilyName="Smith", qb=client) -``` + + customers = Customer.filter(Active=True, FamilyName="Smith", qb=client) Filtered list of objects with ordering: -```python -# Get customer invoices ordered by TxnDate -invoices = Invoice.filter(CustomerRef='100', order_by='TxnDate', qb=client) -# Same, but in reverse order -invoices = Invoice.filter(CustomerRef='100', order_by='TxnDate DESC', qb=client) + # Get customer invoices ordered by TxnDate + invoices = Invoice.filter(CustomerRef='100', order_by='TxnDate', qb=client) + + # Same, but in reverse order + invoices = Invoice.filter(CustomerRef='100', order_by='TxnDate DESC', qb=client) + + # Order customers by FamilyName then by GivenName + customers = Customer.all(order_by='FamilyName, GivenName', qb=client) -# Order customers by FamilyName then by GivenName -customers = Customer.all(order_by='FamilyName, GivenName', qb=client) -``` Filtered list of objects with paging: -```python -customers = Customer.filter(start_position=1, max_results=25, Active=True, FamilyName="Smith", qb=client) -``` + + customers = Customer.filter(start_position=1, max_results=25, Active=True, FamilyName="Smith", qb=client) + List Filtered by values in list: -```python -customer_names = ['Customer1', 'Customer2', 'Customer3'] -customers = Customer.choose(customer_names, field="DisplayName", qb=client) -``` + + customer_names = ['Customer1', 'Customer2', 'Customer3'] + customers = Customer.choose(customer_names, field="DisplayName", qb=client) + List with custom Where Clause (do not include the `"WHERE"`): -```python -customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", qb=client) -``` + + customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", qb=client) + + + List with custom Where and ordering -```python -customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", order_by='DisplayName', qb=client) -``` + + customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", order_by='DisplayName', qb=client) + List with custom Where Clause and paging: -```python -customers = Customer.where("CompanyName LIKE 'S%'", start_position=1, max_results=25, qb=client) -``` -Filtering a list with a custom query (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for + + customers = Customer.where("CompanyName LIKE 'S%'", start_position=1, max_results=25, qb=client) + +Filtering a list with a custom query (See [Query operations and syntax](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/data-queries) for supported SQL statements): -```python -customers = Customer.query("SELECT * FROM Customer WHERE Active = True", qb=client) -``` + + customers = Customer.query("SELECT * FROM Customer WHERE Active = True", qb=client) + Filtering a list with a custom query with paging: -```python -customers = Customer.query("SELECT * FROM Customer WHERE Active = True STARTPOSITION 1 MAXRESULTS 25", qb=client) -``` + + customers = Customer.query("SELECT * FROM Customer WHERE Active = True STARTPOSITION 1 MAXRESULTS 25", qb=client) + Get record count (do not include the ``"WHERE"``): -```python -customer_count = Customer.count("Active = True AND CompanyName LIKE 'S%'", qb=client) -``` + + customer_count = Customer.count("Active = True AND CompanyName LIKE 'S%'", qb=client) + Get single object by Id and update: -```python -customer = Customer.get(1, qb=client) -customer.CompanyName = "New Test Company Name" -customer.save(qb=client) -``` + + customer = Customer.get(1, qb=client) + customer.CompanyName = "New Test Company Name" + customer.save(qb=client) + Create new object: -```python -customer = Customer() -customer.CompanyName = "Test Company" -customer.save(qb=client) -``` + + customer = Customer() + customer.CompanyName = "Test Company" + customer.save(qb=client) + Batch Operations ---------------- @@ -143,162 +151,196 @@ operations in a single request (See [Intuit Batch Operations Guide](https://deve full details). Batch create a list of objects: -```python -from quickbooks.batch import batch_create -customer1 = Customer() -customer1.CompanyName = "Test Company 1" + from quickbooks.batch import batch_create -customer2 = Customer() -customer2.CompanyName = "Test Company 2" + customer1 = Customer() + customer1.CompanyName = "Test Company 1" -customers = [customer1, customer2] + customer2 = Customer() + customer2.CompanyName = "Test Company 2" + + customers = [customer1, customer2] + + results = batch_create(customers, qb=client) -results = batch_create(customers, qb=client) -``` Batch update a list of objects: -```python -from quickbooks.batch import batch_update -customers = Customer.filter(Active=True) -# Update customer records -results = batch_update(customers, qb=client) -``` -Batch delete a list of objects: -```python -from quickbooks.batch import batch_delete + from quickbooks.batch import batch_update + customers = Customer.filter(Active=True) + + # Update customer records + + results = batch_update(customers, qb=client) + +Batch delete a list of objects (only entities that support delete can use batch delete): + + from quickbooks.batch import batch_delete + + payments = Payment.filter(TxnDate=date.today()) + results = batch_delete(payments, qb=client) -customers = Customer.filter(Active=False) -results = batch_delete(customers, qb=client) -``` Review results for batch operation: -```python -# successes is a list of objects that were successfully updated -for obj in results.successes: - print("Updated " + obj.DisplayName) -# faults contains list of failed operations and associated errors -for fault in results.faults: - print("Operation failed on " + fault.original_object.DisplayName) + # successes is a list of objects that were successfully updated + for obj in results.successes: + print("Updated " + obj.DisplayName) + + # faults contains list of failed operations and associated errors + for fault in results.faults: + print("Operation failed on " + fault.original_object.DisplayName) + + for error in fault.Error: + print("Error " + error.Message) - for error in fault.Error: - print("Error " + error.Message) -``` Change Data Capture ----------------------- Change Data Capture returns a list of objects that have changed since a given time (see [Change data capture](https://developer.intuit.com/docs/api/accounting/changedatacapture) for more details): -```python -from quickbooks.cdc import change_data_capture -from quickbooks.objects import Invoice -cdc_response = change_data_capture([Invoice], "2017-01-01T00:00:00", qb=client) -for invoice in cdc_response.Invoice: - pass # Do something with the invoice -``` + from quickbooks.cdc import change_data_capture + from quickbooks.objects import Invoice + + cdc_response = change_data_capture([Invoice], "2017-01-01T00:00:00", qb=client) + for invoice in cdc_response.Invoice: + # Do something with the invoice + Querying muliple entity types at the same time: -```python -from quickbooks.objects import Invoice, Customer -cdc_response = change_data_capture([Invoice, Customer], "2017-01-01T00:00:00", qb=client) -``` + + from quickbooks.objects import Invoice, Customer + cdc_response = change_data_capture([Invoice, Customer], "2017-01-01T00:00:00", qb=client) + If you use a `datetime` object for the timestamp, it is automatically converted to a string: -```python -from datetime import datetime -cdc_response = change_data_capture([Invoice, Customer], datetime(2017, 1, 1, 0, 0, 0), qb=client) -``` + from datetime import datetime + + cdc_response = change_data_capture([Invoice, Customer], datetime(2017, 1, 1, 0, 0, 0), qb=client) + Attachments ---------------- See [Attachable documentation](https://developer.intuit.com/docs/api/accounting/Attachable) for list of valid file types, file size limits and other restrictions. Attaching a note to a customer: -```python -attachment = Attachable() -attachable_ref = AttachableRef() -attachable_ref.EntityRef = customer.to_ref() + attachment = Attachable() -attachment.AttachableRef.append(attachable_ref) + attachable_ref = AttachableRef() + attachable_ref.EntityRef = customer.to_ref() + + attachment.AttachableRef.append(attachable_ref) + + attachment.Note = 'This is a note' + attachment.save(qb=client) -attachment.Note = 'This is a note' -attachment.save(qb=client) -``` Attaching a file to customer: -```python -attachment = Attachable() -attachable_ref = AttachableRef() -attachable_ref.EntityRef = customer.to_ref() + attachment = Attachable() -attachment.AttachableRef.append(attachable_ref) + attachable_ref = AttachableRef() + attachable_ref.EntityRef = customer.to_ref() -attachment.FileName = 'Filename' -attachment._FilePath = '/folder/filename' # full path to file -attachment.ContentType = 'application/pdf' -attachment.save(qb=client) -``` -Other operations + attachment.AttachableRef.append(attachable_ref) + + attachment.FileName = 'Filename' + attachment._FilePath = '/folder/filename' # full path to file + 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 ---------------- -Void an invoice: -```python -invoice = Invoice() -invoice.Id = 7 -invoice.void(qb=client) -``` -If your consumer_key never changes you can enable the client to stay running: -```python -QuickBooks.enable_global() -``` -You can disable the global client like so: -```python -QuickBooks.disable_global() -``` +Some QBO objects have options that need to be set on the query string of an API call. +One example is `include=allowduplicatedocnum` on the Purchase object. You can add these params when calling save: + + purchase.save(qb=self.qb_client, params={'include': 'allowduplicatedocnum'}) + +Sharable Invoice Link +---------------- +To add a sharable link for an invoice, make sure the AllowOnlineCreditCardPayment is set to True and BillEmail is set to a invalid email address: + + invoice.AllowOnlineCreditCardPayment = True + invoice.BillEmail = EmailAddress() + invoice.BillEmail.Address = 'test@email.com' + +When you query the invoice include the following params (minorversion must be set to 36 or greater): + + invoice = Invoice.get(id, qb=self.qb_client, params={'include': 'invoiceLink'}) + + +Void an invoice +---------------- +Call `void` on any invoice with an Id: + + invoice = Invoice() + invoice.Id = 7 + invoice.void(qb=client) + Working with JSON data ---------------- All objects include `to_json` and `from_json` methods. Converting an object to JSON data: -```python -account = Account.get(1, qb=client) -json_data = account.to_json() -``` + + account = Account.get(1, qb=client) + json_data = account.to_json() + Loading JSON data into a quickbooks object: -```python -account = Account() -account.from_json({ - "AccountType": "Accounts Receivable", - "Name": "MyJobs" -}) -account.save(qb=client) -``` + + account = Account.from_json( + { + "AccountType": "Accounts Receivable", + "AcctNum": "123123", + "Name": "MyJobs" + } + ) + account.save(qb=client) + Date formatting ---------------- When setting date or datetime fields, Quickbooks requires a specific format. Formating helpers are available in helpers.py. Example usage: -```python -date_string = qb_date_format(date(2016, 7, 22)) -date_time_string = qb_datetime_format(datetime(2016, 7, 22, 10, 35, 00)) -date_time_with_utc_string = qb_datetime_utc_offset_format(datetime(2016, 7, 22, 10, 35, 00), '-06:00') -``` + + date_string = qb_date_format(date(2016, 7, 22)) + date_time_string = qb_datetime_format(datetime(2016, 7, 22, 10, 35, 00)) + date_time_with_utc_string = qb_datetime_utc_offset_format(datetime(2016, 7, 22, 10, 35, 00), '-06:00') + Exception Handling ---------------- The QuickbooksException object contains additional [QBO error code](https://developer.intuit.com/app/developer/qbo/docs/develop/troubleshooting/error-codes#id1) information. -```python -from quickbooks.exceptions import QuickbooksException -try: - pass # perform a Quickbooks operation -except QuickbooksException as e: - e.message # contains the error message returned from QBO - e.error_code # contains the - e.detail # contains additional information when available -``` + from quickbooks.exceptions import QuickbooksException + + try: + # perform a Quickbooks operation + except QuickbooksException as e: + e.message # contains the error message returned from QBO + e.error_code # contains the + e.detail # contains additional information when available + **Note:** Objects and object property names match their Quickbooks counterparts and do not follow PEP8. **Note:** This is a work-in-progress made public to help other developers access the QuickBooks API. Built for a Django project. + + + diff --git a/contributing.md b/contributing.md index ae676b08..081f6060 100644 --- a/contributing.md +++ b/contributing.md @@ -1,10 +1,10 @@ # Contributing -I am accepting pull requests. Sometimes life gets busy and it takes me a little while to get everything merged in. To help speed up the process, please write tests to cover your changes. I will review/merge them as soon as possible. +I am accepting pull requests. Sometimes life gets busy and it takes me a little while to get everything reviewed and merged in. To help speed up the process, please write tests to cover your changes. I will review/merge them as soon as possible. # Testing -I use [nose](https://nose.readthedocs.io/en/latest/index.html) and [Coverage](https://coverage.readthedocs.io/en/latest/) to run the test suite. +I use [pytest](https://docs.pytest.org/en/7.4.x/contents.html), [Coverage](https://coverage.readthedocs.io/en/latest/), and [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) to run the test suite. *WARNING*: The Tests connect to the QBO API and create/modify/delete data. DO NOT USE A PRODUCTION ACCOUNT! @@ -12,24 +12,27 @@ I use [nose](https://nose.readthedocs.io/en/latest/index.html) and [Coverage](ht 1. Create/login into your [Intuit Developer account](https://developer.intuit.com). 2. On your Intuit Developer account, create a Sandbox company and an App. -3. Go to the Intuit Developer OAuth 2.0 Playground and fill out the form to get an **access token** and **refresh token**. You will need to copy the following values into your enviroment variables: +3. Go to the Intuit Developer OAuth 2.0 Playground and fill out the form to get a **refresh token**. You will need to copy the following values into your enviroment variables: ``` export CLIENT_ID="" export CLIENT_SECRET="" - export COMPANY_ID="" - export ACCESS_TOKEN="" + export COMPANY_ID="" export REFRESH_TOKEN="" ``` - *Note*: You will need to update the access token when it expires. + *Note*: You will need to update the refresh token when it expires. -5. Install *nose* and *coverage*. Using Pip: - `pip install nose coverage` +5. Install *pytest*, *coverage*, and *pytest-cov*. Using Pip (or whatever): + `pip install pytest coverage pytest-cov` -6. Run `nosetests . --with-coverage --cover-package=quickbooks` +6. Run all tests: ```pytest --cov``` + Run only unit tests: ```pytest tests/unit --cov``` + Run only integration tests: ```pytest tests/integration --cov``` + + ## Creating new tests -Normal Unit tests that do not connect to the QBO API should be located under `test/unit` Test that connect to QBO API should go under `tests/integration`. Inheriting from `QuickbooksTestCase` will automatically setup `self.qb_client` to use when connecting to QBO. +Normal Unit tests that do not connect to the QBO API should be located under `test/unit`. Tests that connect to QBO API should go under `tests/integration`. Inheriting from `QuickbooksTestCase` will automatically setup `self.qb_client` to use when connecting to QBO. Example: ``` diff --git a/dev_requirements.txt b/dev_requirements.txt index 65190a33..05c1b035 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,3 @@ -coverage==4.5.1 -ipdb==0.11 -mock==2.0.0 +coverage==7.3.0 +ipdb==0.13.13 nose==1.3.7 \ No newline at end of file diff --git a/quickbooks/client.py b/quickbooks/client.py index c882169c..a4280444 100644 --- a/quickbooks/client.py +++ b/quickbooks/client.py @@ -1,28 +1,17 @@ -try: # Python 3 - import http.client as httplib - from urllib.parse import parse_qsl - from functools import partial - to_bytes = lambda value, *args, **kwargs: bytes(value, "utf-8", *args, **kwargs) -except ImportError: # Python 2 - import httplib - from urlparse import parse_qsl - to_bytes = str - +import http.client as httplib import textwrap -import codecs import json - -from . import exceptions import base64 import hashlib import hmac +import decimal +import warnings -try: - from rauth import OAuth1Session, OAuth1Service, OAuth2Session -except ImportError: - print("Please import Rauth:\n\n") - print("http://rauth.readthedocs.org/en/latest//n") - raise +from . import exceptions +from requests_oauthlib import OAuth2Session + +def to_bytes(value, *args, **kwargs): + return bytes(value, "utf-8", *args, **kwargs) class Environments(object): @@ -31,12 +20,15 @@ class Environments(object): class QuickBooks(object): + MINIMUM_MINOR_VERSION = 75 company_id = 0 session = None auth_client = None sandbox = False minorversion = None verifier_token = None + invoice_link = False + use_decimal = False sandbox_api_url_v3 = "https://sandbox-quickbooks.api.intuit.com/v3" api_url_v3 = "https://quickbooks.api.intuit.com/v3" @@ -44,12 +36,13 @@ class QuickBooks(object): _BUSINESS_OBJECTS = [ "Account", "Attachable", "Bill", "BillPayment", - "Class", "CreditMemo", "Customer", "CompanyCurrency", - "Department", "Deposit", "Employee", "Estimate", "Invoice", - "Item", "JournalEntry", "Payment", "PaymentMethod", + "Class", "CreditMemo", "Customer", "CustomerType", "CompanyCurrency", + "Department", "Deposit", "Employee", "Estimate", "ExchangeRate", "Invoice", + "Item", "JournalEntry", "Payment", "PaymentMethod", "Preferences", "Purchase", "PurchaseOrder", "RefundReceipt", "SalesReceipt", "TaxAgency", "TaxCode", "TaxService/Taxcode", "TaxRate", "Term", "TimeActivity", "Transfer", "Vendor", "VendorCredit", "CreditCardPayment", + "RecurringTransaction" ] __instance = None @@ -77,17 +70,34 @@ def __new__(cls, **kwargs): else: instance.sandbox = False - instance._start_session() + refresh_token = instance._start_session() + instance.refresh_token = refresh_token if 'company_id' in kwargs: instance.company_id = kwargs['company_id'] - if 'minorversion' in kwargs: - instance.minorversion = kwargs['minorversion'] + # 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) if 'verifier_token' in kwargs: instance.verifier_token = kwargs.get('verifier_token') + if 'use_decimal' in kwargs: + instance.use_decimal = kwargs.get('use_decimal') + return instance def _start_session(self): @@ -95,29 +105,14 @@ def _start_session(self): self.auth_client.refresh(refresh_token=self.refresh_token) self.session = OAuth2Session( - client_id=self.auth_client.client_id, - client_secret=self.auth_client.client_secret, - access_token=self.auth_client.access_token, + self.auth_client.client_id, + token={ + 'access_token': self.auth_client.access_token, + 'refresh_token': self.auth_client.refresh_token, + } ) - @classmethod - def get_instance(cls): - return cls.__instance - - @classmethod - def disable_global(cls): - """ - Disable use of singleton pattern. - """ - QuickBooks.__use_global = False - QuickBooks.__instance = None - - @classmethod - def enable_global(cls): - """ - Allow use of singleton pattern. - """ - QuickBooks.__use_global = True + return self.auth_client.refresh_token def _drop(self): QuickBooks.__instance = None @@ -162,12 +157,15 @@ 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): + 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 if not request_body: request_body = {} @@ -178,8 +176,7 @@ def make_request(self, request_type, url, request_body=None, content_type='appli 'User-Agent': 'python-quickbooks V3 library' } - if file_path: - attachment = open(file_path, 'rb') + if file_path or file_bytes: url = url.replace('attachable', 'upload') boundary = '-------------PythonMultipartPost' headers.update({ @@ -190,7 +187,11 @@ def make_request(self, request_type, url, request_body=None, content_type='appli 'Connection': 'close' }) - 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'] @@ -219,10 +220,14 @@ def make_request(self, request_type, url, request_body=None, content_type='appli req = self.process_request(request_type, url, headers=headers, params=params, data=request_body) if req.status_code == httplib.UNAUTHORIZED: - raise exceptions.AuthorizationException("Application authentication failed", detail=req.text) + raise exceptions.AuthorizationException( + "Application authentication failed", error_code=req.status_code, detail=req.text) try: - result = req.json() + if (self.use_decimal): + result = json.loads(req.text, parse_float=decimal.Decimal) + else: + result = json.loads(req.text) except: raise exceptions.QuickbooksException("Error reading json response: {0}".format(req.text), 10000) @@ -235,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: @@ -249,13 +260,19 @@ def process_request(self, request_type, url, headers="", params="", data=""): return self.session.request( request_type, url, headers=headers, params=params, data=data) - def get_single_object(self, qbbo, pk): - url = "{0}/company/{1}/{2}/{3}/".format(self.api_url, self.company_id, qbbo.lower(), pk) - result = self.get(url, {}) + 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) + if params is None: + params = {} - return result + return self.get(url, {}, params=params) - def handle_exceptions(self, results): + @staticmethod + def handle_exceptions(results): + """ + Error codes with description in documentation: + https://developer.intuit.com/app/developer/qbo/docs/develop/troubleshooting/error-codes#id1 + """ # Needs to handle multiple errors for error in results["Error"]: @@ -269,32 +286,32 @@ def handle_exceptions(self, results): if "code" in error: code = int(error["code"]) - if code > 0 and code <= 499: + if 0 < code <= 499: raise exceptions.AuthorizationException(message, code, detail) - elif code >= 500 and code <= 599: + elif 500 <= code <= 599: raise exceptions.UnsupportedException(message, code, detail) - elif code >= 600 and code <= 1999: + elif 600 <= code <= 1999: if code == 610: raise exceptions.ObjectNotFoundException(message, code, detail) raise exceptions.GeneralException(message, code, detail) - elif code >= 2000 and code <= 4999: + elif 2000 <= code <= 4999: raise exceptions.ValidationException(message, code, detail) - elif code >= 10000: + elif 10000 <= code: raise exceptions.SevereException(message, code, detail) else: raise exceptions.QuickbooksException(message, code, detail) - def create_object(self, qbbo, request_body, _file_path=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) + results = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params) return results - def query(self, select): + def query(self, select, params=None): url = "{0}/company/{1}/query".format(self.api_url, self.company_id) - result = self.post(url, select, content_type='application/text') + result = self.post(url, select, content_type='application/text', params=params) return result @@ -304,15 +321,18 @@ def isvalid_object_name(self, object_name): return True - def update_object(self, qbbo, request_body, _file_path=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) + 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 - def delete_object(self, qbbo, request_body, _file_path=None): + def delete_object(self, qbbo, request_body, _file_path=None, request_id=None): url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower()) - result = self.post(url, request_body, params={'operation': 'delete'}, file_path=_file_path) + result = self.post(url, request_body, params={'operation': 'delete'}, file_path=_file_path, request_id=request_id) return result @@ -347,7 +367,8 @@ def download_pdf(self, qbbo, item_id): if response.status_code == httplib.UNAUTHORIZED: # Note that auth errors have different result structure which can't be parsed by handle_exceptions() - raise exceptions.AuthorizationException("Application authentication failed", detail=response.text) + raise exceptions.AuthorizationException( + "Application authentication failed", error_code=response.status_code, detail=response.text) try: result = response.json() diff --git a/quickbooks/exceptions.py b/quickbooks/exceptions.py index 1469e550..b002c4f0 100644 --- a/quickbooks/exceptions.py +++ b/quickbooks/exceptions.py @@ -7,47 +7,57 @@ def __init__(self, message, error_code=0, detail=""): self.error_code = error_code self.detail = detail self.message = message - + def __str__(self) -> str: + return f"QB Exception {self.error_code}: {self.message}\n{self.detail}" + def __iter__(self): + yield "error_code", self.error_code + yield "detail", self.detail + yield "message", self.message class AuthorizationException(QuickbooksException): """ Quickbooks Error Codes from 1 to 499 """ def __str__(self): - return "QB Auth Exception: " + self.message + " \n\n" + self.detail + return f"QB Auth Exception {self.error_code}: {self.message}\n{self.detail}" class UnsupportedException(QuickbooksException): """ Quickbooks Error Codes from 500 to 599 """ - pass + def __str__(self): + return f"QB Unsupported Exception {self.error_code}: {self.message}\n{self.detail}" class GeneralException(QuickbooksException): """ Quickbooks Error Codes from 600 to 1999 """ - pass + def __str__(self): + return f"QB General Exception {self.error_code}: {self.message}\n{self.detail}" class ValidationException(QuickbooksException): """ Quickbooks Error Codes from 2000 to 4999 """ - pass + def __str__(self): + return f"QB Validation Exception {self.error_code}: {self.message}\n{self.detail}" class SevereException(QuickbooksException): """ Quickbooks Error Codes greater than 10000 """ - pass + def __str__(self): + return f"QB Severe Exception {self.error_code}: {self.message}\n{self.detail}" class ObjectNotFoundException(QuickbooksException): """ Quickbooks Error Code 610 """ - pass + def __str__(self): + return f"QB Object Not Found Exception {self.error_code}: {self.message}\n{self.detail}" diff --git a/quickbooks/mixins.py b/quickbooks/mixins.py index 791ec69f..60140599 100644 --- a/quickbooks/mixins.py +++ b/quickbooks/mixins.py @@ -1,24 +1,27 @@ -from future.moves.urllib.parse import quote +import decimal +import json +from urllib.parse import quote -try: import simplejson as json -except ImportError: import json - -import six -from .utils import build_where_clause, build_choose_clause from .client import QuickBooks from .exceptions import QuickbooksException +from .utils import build_choose_clause, build_where_clause +class DecimalEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, decimal.Decimal): + return str(o) + return super(DecimalEncoder, self).default(o) class ToJsonMixin(object): def to_json(self): - return json.dumps(self, default=self.json_filter(), sort_keys=True, indent=4) + return json.dumps(self, cls=DecimalEncoder, default=self.json_filter(), sort_keys=True, indent=4) def json_filter(self): """ filter out properties that have names starting with _ or properties that have a value of None """ - return lambda obj: dict((k, v) for k, v in obj.__dict__.items() + return lambda obj: str(obj) if isinstance(obj, decimal.Decimal) else dict((k, v) for k, v in obj.__dict__.items() if not k.startswith('_') and getattr(obj, k) is not None) @@ -70,14 +73,9 @@ def to_dict(obj, classkey=None): elif hasattr(obj, "__iter__") and not isinstance(obj, str): return [to_dict(v, classkey) for v in obj] elif hasattr(obj, "__dict__"): - if six.PY2: - data = dict([(key, to_dict(value, classkey)) - for key, value in obj.__dict__.iteritems() - if not callable(value) and not key.startswith('_')]) - else: - data = dict([(key, to_dict(value, classkey)) - for key, value in obj.__dict__.items() - if not callable(value) and not key.startswith('_')]) + data = dict([(key, to_dict(value, classkey)) + for key, value in obj.__dict__.items() + if not callable(value) and not key.startswith('_')]) if classkey is not None and hasattr(obj, "__class__"): data[classkey] = obj.__class__.__name__ @@ -96,11 +94,11 @@ class ReadMixin(object): qbo_json_object_name = "" @classmethod - def get(cls, id, qb=None): + def get(cls, id, qb=None, params=None): if not qb: qb = QuickBooks() - json_data = qb.get_single_object(cls.qbo_object_name, pk=id) + json_data = qb.get_single_object(cls.qbo_object_name, pk=id, params=params) if cls.qbo_json_object_name != '': return cls.from_json(json_data[cls.qbo_json_object_name]) @@ -125,6 +123,53 @@ def send(self, qb=None, send_to=None): class VoidMixin(object): + + def get_void_params(self): + qb_object_params_map = { + "Payment": { + "operation": "update", + "include": "void" + }, + "SalesReceipt": { + "operation": "update", + "include": "void" + }, + "BillPayment": { + "operation": "update", + "include": "void" + }, + "Invoice": { + "operation": "void", + }, + } + # setting the default operation to void (the original behavior) + return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"}) + + def get_void_data(self): + qb_object_params_map = { + "Payment": { + "Id": self.Id, + "SyncToken": self.SyncToken, + "sparse": True + }, + "SalesReceipt": { + "Id": self.Id, + "SyncToken": self.SyncToken, + "sparse": True + }, + "BillPayment": { + "Id": self.Id, + "SyncToken": self.SyncToken, + "sparse": True + }, + "Invoice": { + "Id": self.Id, + "SyncToken": self.SyncToken, + }, + } + # setting the default operation to void (the original behavior) + return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"}) + def void(self, qb=None): if not qb: qb = QuickBooks() @@ -132,14 +177,12 @@ def void(self, qb=None): if not self.Id: raise QuickbooksException('Cannot void unsaved object') - data = { - 'Id': self.Id, - 'SyncToken': self.SyncToken, - } - endpoint = self.qbo_object_name.lower() url = "{0}/company/{1}/{2}".format(qb.api_url, qb.company_id, endpoint) - results = qb.post(url, json.dumps(data), params={'operation': 'void'}) + + data = self.get_void_data() + params = self.get_void_params() + results = qb.post(url, json.dumps(data, cls=DecimalEncoder), params=params) return results @@ -148,14 +191,14 @@ class UpdateMixin(object): qbo_object_name = "" qbo_json_object_name = "" - def save(self, qb=None): + def save(self, qb=None, request_id=None, params=None): if not qb: qb = QuickBooks() if self.Id and int(self.Id) > 0: - json_data = qb.update_object(self.qbo_object_name, self.to_json()) + json_data = qb.update_object(self.qbo_object_name, self.to_json(), request_id=request_id, params=params) else: - json_data = qb.create_object(self.qbo_object_name, self.to_json()) + json_data = qb.create_object(self.qbo_object_name, self.to_json(), request_id=request_id, params=params) if self.qbo_json_object_name != '': obj = type(self).from_json(json_data[self.qbo_json_object_name]) @@ -166,10 +209,23 @@ def save(self, qb=None): return obj +class UpdateNoIdMixin(object): + qbo_object_name = "" + qbo_json_object_name = "" + + def save(self, qb=None, request_id=None): + if not qb: + qb = QuickBooks() + + json_data = qb.update_object(self.qbo_object_name, self.to_json(), request_id=request_id) + obj = type(self).from_json(json_data[self.qbo_object_name]) + return obj + + class DeleteMixin(object): qbo_object_name = "" - def delete(self, qb=None): + def delete(self, qb=None, request_id=None): if not qb: qb = QuickBooks() @@ -180,7 +236,17 @@ def delete(self, qb=None): 'Id': self.Id, 'SyncToken': self.SyncToken, } - return qb.delete_object(self.qbo_object_name, json.dumps(data)) + return qb.delete_object(self.qbo_object_name, json.dumps(data, cls=DecimalEncoder), request_id=request_id) + + +class DeleteNoIdMixin(object): + qbo_object_name = "" + + def delete(self, qb=None, request_id=None): + if not qb: + qb = QuickBooks() + + return qb.delete_object(self.qbo_object_name, self.to_json(), request_id=request_id) class ListMixin(object): @@ -189,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): @@ -341,3 +419,17 @@ def append(self, value): def pop(self, *args, **kwargs): return self._object_list.pop(*args, **kwargs) + + +class PrefMixin(object): + qbo_object_name = "" + qbo_json_object_name = "" + + @classmethod + def get(cls, qb=None): + if not qb: + qb = QuickBooks() + + end_point = "{0}/company/{1}/preferences".format(qb.api_url, qb.company_id) + json_data = qb.get(end_point, {}) + return cls.from_json(json_data[cls.qbo_object_name]) diff --git a/quickbooks/objects/__init__.py b/quickbooks/objects/__init__.py index 9ee9f167..de319d34 100644 --- a/quickbooks/objects/__init__.py +++ b/quickbooks/objects/__init__.py @@ -21,9 +21,9 @@ ) from .detailline import ( DetailLine, DiscountOverride, DiscountLineDetail, DiscountLine, - SubtotalLineDetail, SubtotalLine, DescriptionLineDetail, DescriptionLine, + SubtotalLineDetail, SubtotalLine, DescriptionLineDetail, DescriptionOnlyLine, SalesItemLineDetail, SalesItemLine, GroupLineDetail, GroupLine, - DescriptionOnlyLine, AccountBasedExpenseLineDetail, AccountBasedExpenseLine, + AccountBasedExpenseLineDetail, AccountBasedExpenseLine, TDSLineDetail, TDSLine, ItemBasedExpenseLineDetail, ItemBasedExpenseLine, ) @@ -36,6 +36,12 @@ ) from .payment import PaymentLine, Payment from .paymentmethod import PaymentMethod +from .preferences import ( + AccountingInfoPrefs, ClassTrackingPerTxnLine, CurrencyPrefs, + EmailMessageType, EmailMessagesPrefs, OtherPrefs, Preferences, + ProductAndServicesPrefs, ReportPrefs, SalesFormsPrefs, + VendorAndPurchasesPrefs, TaxPrefs, TimeTrackingPrefs, +) from .purchase import Purchase from .purchaseorder import PurchaseOrder from .refundreceipt import RefundReceipt diff --git a/quickbooks/objects/account.py b/quickbooks/objects/account.py index 5f30eb25..1c7f7e9f 100644 --- a/quickbooks/objects/account.py +++ b/quickbooks/objects/account.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity -@python_2_unicode_compatible class Account(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: Account is a component of a Chart Of Accounts, and is part of a Ledger. Used to record a total diff --git a/quickbooks/objects/attachable.py b/quickbooks/objects/attachable.py index a45625cd..ccb22609 100644 --- a/quickbooks/objects/attachable.py +++ b/quickbooks/objects/attachable.py @@ -1,10 +1,8 @@ -from six import python_2_unicode_compatible from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, AttachableRef from ..client import QuickBooks from ..mixins import DeleteMixin -@python_2_unicode_compatible class Attachable(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: This page covers the Attachable, Upload, and Download resources used for attachment management. Attachments are supplemental information linked to a transaction or Item object. They can be files, notes, or a combination of both. @@ -29,6 +27,7 @@ def __init__(self): self.AttachableRef = [] self.FileName = None self._FilePath = '' + self._FileBytes = None self.Note = "" self.FileAccessUri = None self.TempDownloadUri = None @@ -55,12 +54,20 @@ 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.FileName: + if self.Id is None and self.FileName: obj = type(self).from_json(json_data['AttachableResponse'][0]['Attachable']) else: obj = type(self).from_json(json_data['Attachable']) diff --git a/quickbooks/objects/base.py b/quickbooks/objects/base.py index b2e2117e..c6b1afd2 100644 --- a/quickbooks/objects/base.py +++ b/quickbooks/objects/base.py @@ -1,5 +1,4 @@ -from six import python_2_unicode_compatible -from ..mixins import ToJsonMixin, FromJsonMixin, ReadMixin, ListMixin, UpdateMixin, ToDictMixin +from ..mixins import ToDictMixin, ToJsonMixin, FromJsonMixin, ListMixin, ReadMixin, UpdateMixin class QuickbooksBaseObject(ToJsonMixin, FromJsonMixin, ToDictMixin): @@ -24,7 +23,6 @@ class QuickbooksReadOnlyObject(QuickbooksBaseObject, ReadMixin, ListMixin): pass -@python_2_unicode_compatible class MetaData(FromJsonMixin): def __init__(self): self.CreateTime = "" @@ -44,7 +42,6 @@ def to_linked_txn(self): return linked_txn -@python_2_unicode_compatible class Address(QuickbooksBaseObject): def __init__(self): self.Id = None @@ -65,7 +62,6 @@ def __str__(self): return "{0} {1}, {2} {3}".format(self.Line1, self.City, self.CountrySubDivisionCode, self.PostalCode) -@python_2_unicode_compatible class PhoneNumber(ToJsonMixin, FromJsonMixin, ToDictMixin): def __init__(self): self.FreeFormNumber = "" @@ -74,7 +70,6 @@ def __str__(self): return self.FreeFormNumber -@python_2_unicode_compatible class EmailAddress(QuickbooksBaseObject): def __init__(self): self.Address = "" @@ -83,7 +78,6 @@ def __str__(self): return self.Address -@python_2_unicode_compatible class WebAddress(QuickbooksBaseObject): def __init__(self): self.URI = "" @@ -92,7 +86,6 @@ def __str__(self): return self.URI -@python_2_unicode_compatible class Ref(QuickbooksBaseObject): def __init__(self): self.value = "" @@ -103,7 +96,6 @@ def __str__(self): return self.name -@python_2_unicode_compatible class CustomField(QuickbooksBaseObject): def __init__(self): self.DefinitionId = "" @@ -115,7 +107,6 @@ def __str__(self): return self.Name -@python_2_unicode_compatible class LinkedTxn(QuickbooksBaseObject): qbo_object_name = "LinkedTxn" @@ -129,7 +120,6 @@ def __str__(self): return str(self.TxnId) -@python_2_unicode_compatible class CustomerMemo(QuickbooksBaseObject): def __init__(self): super(CustomerMemo, self).__init__() diff --git a/quickbooks/objects/batchrequest.py b/quickbooks/objects/batchrequest.py index 520419b3..2f7aecbc 100644 --- a/quickbooks/objects/batchrequest.py +++ b/quickbooks/objects/batchrequest.py @@ -1,4 +1,3 @@ -from six import python_2_unicode_compatible from ..mixins import ToJsonMixin, FromJsonMixin @@ -8,7 +7,6 @@ class BatchOperation(object): DELETE = "delete" -@python_2_unicode_compatible class FaultError(FromJsonMixin): qbo_object_name = "Error" diff --git a/quickbooks/objects/bill.py b/quickbooks/objects/bill.py index 013ac92b..09683bd9 100644 --- a/quickbooks/objects/bill.py +++ b/quickbooks/objects/bill.py @@ -1,14 +1,11 @@ -from six import python_2_unicode_compatible - from quickbooks.objects.detailline import DetailLine, ItemBasedExpenseLine, AccountBasedExpenseLine, \ TDSLine from .base import Ref, LinkedTxn, QuickbooksManagedObject, QuickbooksTransactionEntity, \ - LinkedTxnMixin + LinkedTxnMixin, Address from .tax import TxnTaxDetail from ..mixins import DeleteMixin -@python_2_unicode_compatible class Bill(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: A Bill entity is an AP transaction representing a request-for-payment from a third party for @@ -23,6 +20,7 @@ class Bill(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, Li "AttachableRef": Ref, "DepartmentRef": Ref, "TxnTaxDetail": TxnTaxDetail, + "VendorAddr": Address, } list_dict = { @@ -56,6 +54,7 @@ def __init__(self): self.VendorRef = None self.DepartmentRef = None self.APAccountRef = None + self.VendorAddr = None self.LinkedTxn = [] self.Line = [] @@ -79,4 +78,3 @@ def to_ref(self): ref.value = self.Id return ref - diff --git a/quickbooks/objects/billpayment.py b/quickbooks/objects/billpayment.py index 6fd1e460..64569d90 100644 --- a/quickbooks/objects/billpayment.py +++ b/quickbooks/objects/billpayment.py @@ -1,10 +1,8 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, LinkedTxn, QuickbooksManagedObject, LinkedTxnMixin, \ QuickbooksTransactionEntity -from ..mixins import DeleteMixin +from ..mixins import DeleteMixin, VoidMixin -@python_2_unicode_compatible class CheckPayment(QuickbooksBaseObject): class_dict = { "BankAccountRef": Ref @@ -33,7 +31,6 @@ def __init__(self): self.CCAccountRef = None -@python_2_unicode_compatible class BillPaymentLine(QuickbooksBaseObject): list_dict = { "LinkedTxn": LinkedTxn @@ -50,8 +47,7 @@ def __str__(self): return str(self.Amount) -@python_2_unicode_compatible -class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): +class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin): """ QBO definition: A BillPayment entity represents the financial transaction of payment of bills that the business owner receives from a vendor for goods or services purchased diff --git a/quickbooks/objects/budget.py b/quickbooks/objects/budget.py index 17cc6a53..442bd845 100644 --- a/quickbooks/objects/budget.py +++ b/quickbooks/objects/budget.py @@ -1,9 +1,7 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, QuickbooksTransactionEntity, \ QuickbooksReadOnlyObject -@python_2_unicode_compatible class BudgetDetail(QuickbooksBaseObject): class_dict = { "AccountRef": Ref, @@ -26,7 +24,6 @@ def __str__(self): return str(self.Amount) -@python_2_unicode_compatible class Budget(QuickbooksReadOnlyObject, QuickbooksTransactionEntity): """ QBO definition: The Budget endpoint allows you to retrieve the current state of budgets already set up in the user's @@ -56,4 +53,3 @@ def __init__(self): def __str__(self): return self.Name - diff --git a/quickbooks/objects/changedatacapture.py b/quickbooks/objects/changedatacapture.py index ee628dac..d11a527a 100644 --- a/quickbooks/objects/changedatacapture.py +++ b/quickbooks/objects/changedatacapture.py @@ -2,14 +2,14 @@ class CDCResponse(FromJsonMixin): - qbo_object_name = "CDCResponse" + qbo_object_name = "CDCResponse" - def __init__(self): - super(CDCResponse, self).__init__() + def __init__(self): + super(CDCResponse, self).__init__() class QueryResponse(FromJsonMixin, ObjectListMixin): - qbo_object_name = "QueryResponse" + qbo_object_name = "QueryResponse" - def __init__(self): - super(QueryResponse, self).__init__() + def __init__(self): + super(QueryResponse, self).__init__() diff --git a/quickbooks/objects/company_info.py b/quickbooks/objects/company_info.py index 8f6027da..87ddb60d 100644 --- a/quickbooks/objects/company_info.py +++ b/quickbooks/objects/company_info.py @@ -1,10 +1,8 @@ -from six import python_2_unicode_compatible from .base import Address, PhoneNumber, EmailAddress, WebAddress, \ - QuickbooksReadOnlyObject, Ref + QuickbooksManagedObject, Ref, MetaData -@python_2_unicode_compatible -class CompanyInfo(QuickbooksReadOnlyObject): +class CompanyInfo(QuickbooksManagedObject): """ QBO definition: The CompanyInfo entity contains basic company information. In QuickBooks Online, company info and preferences are displayed in the @@ -21,7 +19,8 @@ class CompanyInfo(QuickbooksReadOnlyObject): "LegalAddr": Address, "PrimaryPhone": PhoneNumber, "Email": EmailAddress, - "WebAddr": WebAddress + "WebAddr": WebAddress, + "MetaData": MetaData } qbo_object_name = "CompanyInfo" @@ -43,6 +42,7 @@ def __init__(self): self.PrimaryPhone = None self.Email = None self.WebAddr = None + self.MetaData = None def __str__(self): return self.CompanyName diff --git a/quickbooks/objects/companycurrency.py b/quickbooks/objects/companycurrency.py index 90d0ff66..20f6b778 100644 --- a/quickbooks/objects/companycurrency.py +++ b/quickbooks/objects/companycurrency.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksManagedObject, QuickbooksTransactionEntity, Ref, CustomField, MetaData -@python_2_unicode_compatible class CompanyCurrency(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: Applicable only for those companies that enable multicurrency, a companycurrency object @@ -37,6 +35,6 @@ def to_ref(self): ref.name = self.Name ref.type = self.qbo_object_name - ref.value = self.Id + ref.value = self.Code return ref diff --git a/quickbooks/objects/creditcardpayment_entity.py b/quickbooks/objects/creditcardpayment_entity.py index d7b83a93..9b4b2877 100644 --- a/quickbooks/objects/creditcardpayment_entity.py +++ b/quickbooks/objects/creditcardpayment_entity.py @@ -1,9 +1,7 @@ -from six import python_2_unicode_compatible -from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin +from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, MetaData from ..mixins import DeleteMixin -@python_2_unicode_compatible class CreditCardPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: A Represents a financial transaction to record a Credit Card balance payment @@ -17,6 +15,8 @@ class CreditCardPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransact class_dict = { "BankAccountRef": Ref, "CreditCardAccountRef": Ref, + "VendorRef": Ref, + "MetaData": MetaData, } qbo_object_name = "CreditCardPayment" @@ -27,9 +27,14 @@ def __init__(self): self.TxnDate = None self.Amount = 0 self.PrivateNote = None + self.Memo = None + self.PrintStatus = None + self.CheckNum = None self.BankAccountRef = None self.CreditCardAccountRef = None + self.VendorRef = None + self.MetaData = None def __str__(self): return str(self.Amount) diff --git a/quickbooks/objects/creditmemo.py b/quickbooks/objects/creditmemo.py index 86db3e23..cbb5831b 100644 --- a/quickbooks/objects/creditmemo.py +++ b/quickbooks/objects/creditmemo.py @@ -1,13 +1,10 @@ -from six import python_2_unicode_compatible - -from quickbooks.objects.detailline import SalesItemLine, SubtotalLine, DiscountLine, DescriptionLine, DetailLine +from quickbooks.objects.detailline import SalesItemLine, SubtotalLine, DiscountLine, DescriptionOnlyLine, DetailLine from .base import Address, EmailAddress, Ref, CustomField, CustomerMemo, QuickbooksManagedObject, \ LinkedTxnMixin, QuickbooksTransactionEntity from .tax import TxnTaxDetail from ..mixins import DeleteMixin -@python_2_unicode_compatible class CreditMemo(DeleteMixin, QuickbooksTransactionEntity, QuickbooksManagedObject, LinkedTxnMixin): """ QBO definition: The CreditMemo is a financial transaction representing a refund or credit of payment or part @@ -38,7 +35,7 @@ class CreditMemo(DeleteMixin, QuickbooksTransactionEntity, QuickbooksManagedObje "SalesItemLineDetail": SalesItemLine, "SubTotalLineDetail": SubtotalLine, "DiscountLineDetail": DiscountLine, - "DescriptionLineDetail": DescriptionLine + "DescriptionLineDetail": DescriptionOnlyLine } qbo_object_name = "CreditMemo" @@ -73,3 +70,11 @@ def __init__(self): def __str__(self): return str(self.TotalAmt) + + def to_ref(self): + ref = Ref() + + ref.type = self.qbo_object_name + ref.value = self.Id + + return ref diff --git a/quickbooks/objects/customer.py b/quickbooks/objects/customer.py index 0f43bfd9..87bf95aa 100644 --- a/quickbooks/objects/customer.py +++ b/quickbooks/objects/customer.py @@ -1,9 +1,7 @@ -from six import python_2_unicode_compatible from .base import Address, PhoneNumber, EmailAddress, WebAddress, Ref, QuickbooksManagedObject, \ QuickbooksTransactionEntity -@python_2_unicode_compatible class Customer(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: A customer is a consumer of the service or product that your business offers. The Customer object @@ -48,6 +46,7 @@ def __init__(self): self.PrintOnCheckName = "" self.Notes = "" self.Active = True + self.IsProject = False self.Job = False self.BillWithParent = False self.Taxable = True @@ -72,6 +71,7 @@ def __init__(self): self.PaymentMethodRef = None self.ParentRef = None self.ARAccountRef = None + self.CurrencyRef = None def __str__(self): return self.DisplayName diff --git a/quickbooks/objects/customertype.py b/quickbooks/objects/customertype.py new file mode 100644 index 00000000..af41b3a1 --- /dev/null +++ b/quickbooks/objects/customertype.py @@ -0,0 +1,26 @@ +from .base import Address, PhoneNumber, EmailAddress, WebAddress, MetaData, QuickbooksReadOnlyObject, \ + QuickbooksTransactionEntity + + +class CustomerType(QuickbooksReadOnlyObject, QuickbooksTransactionEntity): + """ + QBO definition: Customer types allow categorizing customers in ways that are meaningful to the business. + For example, one could set up customer types so that they indicate which industry a customer represents, + a customer's geographic location, or how a customer first heard about the business. The categorization + then can be used for reporting or mailings. + """ + + class_dict = { + "MetaData": MetaData + } + + qbo_object_name = "CustomerType" + + def __init__(self): + super(CustomerType, self).__init__() + self.Name = "" + self.Active = False + self.MetaData = None + + def __str__(self): + return self.Name diff --git a/quickbooks/objects/department.py b/quickbooks/objects/department.py index 60465f9b..f81ce49a 100644 --- a/quickbooks/objects/department.py +++ b/quickbooks/objects/department.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksManagedObject, QuickbooksTransactionEntity, Ref -@python_2_unicode_compatible class Department(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: The Department entity provides a way to track different segments of the business, divisions, or diff --git a/quickbooks/objects/deposit.py b/quickbooks/objects/deposit.py index 1b00a039..a8b0c54f 100644 --- a/quickbooks/objects/deposit.py +++ b/quickbooks/objects/deposit.py @@ -1,4 +1,3 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, LinkedTxn, QuickbooksManagedObject, LinkedTxnMixin, \ QuickbooksTransactionEntity, CustomField, AttachableRef from ..mixins import DeleteMixin @@ -35,7 +34,6 @@ def __init__(self): self.PaymentMethodRef = None -@python_2_unicode_compatible class DepositLine(QuickbooksBaseObject): class_dict = { "DepositToAccountRef": Ref, @@ -63,7 +61,6 @@ def __str__(self): return str(self.Amount) -@python_2_unicode_compatible class Deposit(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: A deposit object is a transaction that records one or more deposits of the following types: diff --git a/quickbooks/objects/detailline.py b/quickbooks/objects/detailline.py index 4b484bc4..cf4ef575 100644 --- a/quickbooks/objects/detailline.py +++ b/quickbooks/objects/detailline.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, CustomField, LinkedTxn, MarkupInfo -@python_2_unicode_compatible class DetailLine(QuickbooksBaseObject): list_dict = { "LinkedTxn": LinkedTxn, @@ -53,6 +51,7 @@ def __init__(self): self.Discount = None self.ClassRef = None self.TaxCodeRef = None + self.DiscountAccountRef = None self.PercentBased = False self.DiscountPercent = 0 @@ -100,21 +99,10 @@ def __init__(self): self.TaxCodeRef = None -class DescriptionLine(DetailLine): - class_dict = { - "DescriptionLineDetail": DescriptionLineDetail - } - - def __init__(self): - super(DescriptionLine, self).__init__() - self.DetailType = "DescriptionOnly" - self.DescriptionLineDetail = None - - -@python_2_unicode_compatible class SalesItemLineDetail(QuickbooksBaseObject): class_dict = { "ItemRef": Ref, + "ItemAccountRef": Ref, "ClassRef": Ref, "TaxCodeRef": Ref, "PriceLevelRef": Ref, @@ -130,6 +118,7 @@ def __init__(self): self.MarkupInfo = None self.ItemRef = None + self.ItemAccountRef = None self.ClassRef = None self.TaxCodeRef = None self.PriceLevelRef = None @@ -175,7 +164,6 @@ def __init__(self): self.DescriptionLineDetail = None -@python_2_unicode_compatible class AccountBasedExpenseLineDetail(QuickbooksBaseObject): class_dict = { "CustomerRef": Ref, @@ -188,12 +176,13 @@ class AccountBasedExpenseLineDetail(QuickbooksBaseObject): def __init__(self): super(AccountBasedExpenseLineDetail, self).__init__() self.BillableStatus = None - self.TaxAmount = 0 self.TaxInclusiveAmt = 0 self.CustomerRef = None self.AccountRef = None self.TaxCodeRef = None + self.ClassRef = None + self.MarkupInfo = None def __str__(self): return self.BillableStatus @@ -211,7 +200,6 @@ def __init__(self): self.AccountBasedExpenseLineDetail = None -@python_2_unicode_compatible class TDSLineDetail(QuickbooksBaseObject): def __init__(self): super(TDSLineDetail, self).__init__() @@ -247,8 +235,8 @@ def __init__(self): super(ItemBasedExpenseLineDetail, self).__init__() self.BillableStatus = None self.UnitPrice = 0 - self.TaxInclusiveAmt = 0 self.Qty = 0 + self.TaxInclusiveAmt = 0 self.ItemRef = None self.ClassRef = None self.PriceLevelRef = None diff --git a/quickbooks/objects/employee.py b/quickbooks/objects/employee.py index d9f49adf..64d4e0ff 100644 --- a/quickbooks/objects/employee.py +++ b/quickbooks/objects/employee.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible -from .base import Address, PhoneNumber, QuickbooksManagedObject, QuickbooksTransactionEntity, Ref +from .base import Address, PhoneNumber, QuickbooksManagedObject, QuickbooksTransactionEntity, Ref, EmailAddress -@python_2_unicode_compatible class Employee(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: Employee represents the people who are working for the company. @@ -10,14 +8,16 @@ class Employee(QuickbooksManagedObject, QuickbooksTransactionEntity): class_dict = { "PrimaryAddr": Address, - "PrimaryPhone": PhoneNumber + "PrimaryPhone": PhoneNumber, + "Mobile": PhoneNumber, + "PrimaryEmailAddr": EmailAddress, } qbo_object_name = "Employee" def __init__(self): super(Employee, self).__init__() - self.SSN = "" + self.SSN = None self.GivenName = "" self.FamilyName = "" @@ -28,15 +28,19 @@ def __init__(self): self.EmployeeNumber = "" self.Title = "" self.BillRate = 0 - self.BirthDate = "" + self.CostRate = 0 + self.BirthDate = None self.Gender = None - self.HiredDate = "" + self.HiredDate = None self.ReleasedDate = "" self.Active = True self.Organization = False self.BillableTime = False self.PrimaryAddr = None + self.PrimaryPhone = None + self.Mobile = None + self.EmailAddress = None def __str__(self): return self.DisplayName diff --git a/quickbooks/objects/estimate.py b/quickbooks/objects/estimate.py index 92096870..362937fc 100644 --- a/quickbooks/objects/estimate.py +++ b/quickbooks/objects/estimate.py @@ -1,12 +1,10 @@ -from six import python_2_unicode_compatible from .base import CustomField, Ref, CustomerMemo, Address, EmailAddress, QuickbooksManagedObject, \ LinkedTxnMixin, QuickbooksTransactionEntity, LinkedTxn from .tax import TxnTaxDetail -from .detailline import DetailLine, SalesItemLine, GroupLine, DescriptionLine, DiscountLine, SubtotalLine +from .detailline import DetailLine, SalesItemLine, GroupLine, DescriptionOnlyLine, DiscountLine, SubtotalLine from ..mixins import QuickbooksPdfDownloadable, DeleteMixin, SendMixin -@python_2_unicode_compatible class Estimate(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject, @@ -21,7 +19,9 @@ class Estimate(DeleteMixin, class_dict = { "BillAddr": Address, "ShipAddr": Address, + "ShipFromAddr": Address, "CustomerRef": Ref, + "ProjectRef": Ref, "TxnTaxDetail": TxnTaxDetail, "CustomerMemo": CustomerMemo, "BillEmail": EmailAddress, @@ -41,7 +41,7 @@ class Estimate(DeleteMixin, detail_dict = { "SalesItemLineDetail": SalesItemLine, "GroupLineDetail": GroupLine, - "DescriptionOnly": DescriptionLine, + "DescriptionOnly": DescriptionOnlyLine, "DiscountLineDetail": DiscountLine, "SubTotalLineDetail": SubtotalLine, } @@ -66,14 +66,18 @@ def __init__(self): self.AcceptedDate = None self.GlobalTaxCalculation = "TaxExcluded" self.BillAddr = None + self.DepartmentRef = None self.ShipAddr = None + self.ShipFromAddr = None self.BillEmail = None self.CustomerRef = None + self.ProjectRef = None self.TxnTaxDetail = None self.CustomerMemo = None self.ClassRef = None self.SalesTermRef = None self.ShipMethodRef = None + self.TrackingNum = "" self.CustomField = [] self.LinkedTxn = [] diff --git a/quickbooks/objects/exchangerate.py b/quickbooks/objects/exchangerate.py new file mode 100644 index 00000000..4168c9b9 --- /dev/null +++ b/quickbooks/objects/exchangerate.py @@ -0,0 +1,36 @@ +from quickbooks.mixins import ListMixin, UpdateNoIdMixin, FromJsonMixin +from .base import CustomField, QuickbooksBaseObject + + +class ExchangeRateMetaData(FromJsonMixin): + def __init__(self): + self.LastUpdatedTime = "" + + +class ExchangeRate(QuickbooksBaseObject, ListMixin, UpdateNoIdMixin): + """ + QBO definition: Applicable only for those companies that enable multicurrency, + the exchangerate resource provides the ability to query and set exchange rates available to the + QuickBooks Online company. This entity works in combination with the companycurrency entity + and the Currency Center in the QuickBooks Online UI to manage exchange rates for the company. + """ + + class_dict = { + "MetaData": ExchangeRateMetaData, + "CustomField": CustomField, + } + + qbo_object_name = "ExchangeRate" + + def __str__(self): + return self.SourceCurrencyCode + + def __init__(self): + super(ExchangeRate, self).__init__() + + self.AsOfDate = "" + self.SourceCurrencyCode = "" + self.Rate = 0 + self.TargetCurrencyCode = "" + self.MetaData = None + self.CustomField = None diff --git a/quickbooks/objects/invoice.py b/quickbooks/objects/invoice.py index c4ce710e..8e1d1a98 100644 --- a/quickbooks/objects/invoice.py +++ b/quickbooks/objects/invoice.py @@ -1,6 +1,5 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, CustomField, Address, EmailAddress, CustomerMemo, QuickbooksManagedObject, \ - QuickbooksTransactionEntity, LinkedTxn, LinkedTxnMixin + QuickbooksTransactionEntity, LinkedTxn, LinkedTxnMixin, MetaData from .tax import TxnTaxDetail from .detailline import DetailLine, SalesItemLine, SubtotalLine, DiscountLine, GroupLine, DescriptionOnlyLine from ..mixins import QuickbooksPdfDownloadable, DeleteMixin, SendMixin, VoidMixin @@ -13,7 +12,6 @@ def __init__(self): self.DeliveryTime = "" -@python_2_unicode_compatible class Invoice(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, SendMixin, VoidMixin): """ @@ -33,8 +31,13 @@ class Invoice(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject, Q "ShipAddr": Address, "TxnTaxDetail": TxnTaxDetail, "BillEmail": EmailAddress, + "BillEmailCc": EmailAddress, + "BillEmailBcc": EmailAddress, "CustomerMemo": CustomerMemo, - "DeliveryInfo": DeliveryInfo + "DeliveryInfo": DeliveryInfo, + "RecurDataRef": Ref, + "TaxExemptionRef": Ref, + "MetaData": MetaData } list_dict = { @@ -74,18 +77,28 @@ def __init__(self): self.ExchangeRate = 1 self.GlobalTaxCalculation = "TaxExcluded" self.InvoiceLink = "" + self.HomeBalance = 0 + self.HomeTotalAmt = 0 + self.FreeFormAddress = False self.EInvoiceStatus = None self.BillAddr = None self.ShipAddr = None self.BillEmail = None + self.BillEmailCc = None + self.BillEmailBcc = None self.CustomerRef = None self.CurrencyRef = None self.CustomerMemo = None self.DepartmentRef = None self.TxnTaxDetail = None self.DeliveryInfo = None + self.RecurDataRef = None + self.SalesTermRef = None + self.ShipMethodRef = None + self.TaxExemptionRef = None + self.MetaData = None self.CustomField = [] self.Line = [] diff --git a/quickbooks/objects/item.py b/quickbooks/objects/item.py index 123b8484..c8e6ad19 100644 --- a/quickbooks/objects/item.py +++ b/quickbooks/objects/item.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity -@python_2_unicode_compatible class Item(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: An item is a thing that your company buys, sells, or re-sells, diff --git a/quickbooks/objects/journalentry.py b/quickbooks/objects/journalentry.py index 0380bfe0..d1c5e9fa 100644 --- a/quickbooks/objects/journalentry.py +++ b/quickbooks/objects/journalentry.py @@ -1,4 +1,3 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, \ LinkedTxnMixin from .tax import TxnTaxDetail @@ -51,7 +50,6 @@ def __init__(self): self.JournalEntryLineDetail = None -@python_2_unicode_compatible class JournalEntry(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: Journal Entry is a transaction in which: @@ -83,7 +81,7 @@ def __init__(self): super(JournalEntry, self).__init__() self.Adjustment = False self.TxnDate = "" - #self.TxnSource = "" + # self.TxnSource = "" self.DocNumber = "" self.PrivateNote = "" self.TotalAmt = 0 diff --git a/quickbooks/objects/payment.py b/quickbooks/objects/payment.py index 40c37916..5783d169 100644 --- a/quickbooks/objects/payment.py +++ b/quickbooks/objects/payment.py @@ -1,11 +1,11 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, LinkedTxn, \ - QuickbooksManagedObject, QuickbooksTransactionEntity + QuickbooksManagedObject, QuickbooksTransactionEntity, \ + LinkedTxnMixin, MetaData +from ..client import QuickBooks from .creditcardpayment import CreditCardPayment -from ..mixins import DeleteMixin +from ..mixins import DeleteMixin, VoidMixin -@python_2_unicode_compatible class PaymentLine(QuickbooksBaseObject): list_dict = { "LinkedTxn": LinkedTxn, @@ -20,8 +20,7 @@ def __str__(self): return str(self.Amount) -@python_2_unicode_compatible -class Payment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity): +class Payment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin): """ QBO definition: A Payment entity records a payment in QuickBooks. The payment can be applied for a particular customer against multiple Invoices and Credit Memos. It can also @@ -47,6 +46,8 @@ class Payment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity) "DepositToAccountRef": Ref, "CurrencyRef": Ref, "CreditCardPayment": CreditCardPayment, + "TaxExemptionRef": Ref, + "MetaData": MetaData } list_dict = { @@ -72,6 +73,8 @@ def __init__(self): self.CurrencyRef = None # Readonly self.PaymentMethodRef = None self.DepositToAccountRef = None + self.TaxExemptionRef = None + self.MetaData = None self.Line = [] # These fields are for minor version 4 diff --git a/quickbooks/objects/paymentmethod.py b/quickbooks/objects/paymentmethod.py index dd4da4c5..e33089c6 100644 --- a/quickbooks/objects/paymentmethod.py +++ b/quickbooks/objects/paymentmethod.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksManagedObject, QuickbooksTransactionEntity, Ref -@python_2_unicode_compatible class PaymentMethod(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: The PaymentMethod entity provides the method of payment for received goods. Delete is achieved by setting the @@ -31,4 +29,3 @@ def to_ref(self): ref.value = self.Id return ref - diff --git a/quickbooks/objects/preferences.py b/quickbooks/objects/preferences.py new file mode 100644 index 00000000..49aa23c9 --- /dev/null +++ b/quickbooks/objects/preferences.py @@ -0,0 +1,240 @@ +from quickbooks.mixins import PrefMixin, UpdateNoIdMixin +from .base import QuickbooksBaseObject, QuickbooksTransactionEntity, Ref, EmailAddress + + +class PreferencesCustomField(QuickbooksBaseObject): + def __init__(self): + self.Type = "" + self.Name = "" + self.StringValue = "" + self.BooleanValue = "" + + def __str__(self): + return self.Name + + +class PreferencesCustomFieldGroup(QuickbooksBaseObject): + list_dict = { + "CustomField": PreferencesCustomField + } + + def __init__(self): + super().__init__() + + +class EmailMessageType(QuickbooksBaseObject): + def __init__(self): + super().__init__() + self.Message = "" + self.Subject = "" + + +class EmailMessagesPrefs(QuickbooksBaseObject): + class_dict = { + "InvoiceMessage": EmailMessageType, + "EstimateMessage": EmailMessageType, + "SalesReceiptMessage": EmailMessageType, + "StatementMessage": EmailMessageType, + } + + def __init__(self): + super().__init__() + self.InvoiceMessage = None + self.EstimateMessage = None + self.SalesReceiptMessage = None + self.StatementMessage = None + + +class ProductAndServicesPrefs(QuickbooksBaseObject): + + def __init__(self): + super().__init__() + self.QuantityWithPriceAndRate = True + self.ForPurchase = True + self.QuantityOnHand = True + self.ForSales = True + self.RevenueRecognition = True + self.RevenueRecognitionFrequency = "" + + +class ReportPrefs(QuickbooksBaseObject): + + def __init__(self): + super().__init__() + self.ReportBasis = "Accrual" # or "Cash" + self.CalcAgingReportFromTxnDate = False # read only + + +class AccountingInfoPrefs(QuickbooksBaseObject): + def __init__(self): + super().__init__() + self.FirstMonthOfFiscalYear = "January" # read only + self.UseAccountNumbers = True # read only + self.TaxYearMonth = "January" # read only + self.ClassTrackingPerTxn = False + self.TrackDepartments = False + self.TaxForm = "6" + # Possible values include: Clients, Customers, Donors, Guests, Members, Patients, Tenants. + self.CustomerTerminology = "" # Customers + self.BookCloseDate = "" # e.g. "2018-12-31" + # Possible values include: Business, Department, Division, Location, Property, Store, Territory + self.DepartmentTerminology = "" # Location + self.ClassTrackingPerTxnLine = True + + +class ClassTrackingPerTxnLine(QuickbooksBaseObject): + def __init__(self): + super().__init__() + self.ReportBasis = "Accrual" # or "Cash" + self.CalcAgingReportFromTxnDate = False # read only + + +class SalesFormsPrefs(QuickbooksBaseObject): + class_dict = { + "DefaultTerms": Ref, + "SalesEmailBcc": EmailAddress, + "SalesEmailCc": EmailAddress + } + detail_dict = { + "CustomField": PreferencesCustomFieldGroup + } + + def __init__(self): + super().__init__() + self.ETransactionPaymentEnabled = False + self.CustomTxnNumbers = False + self.AllowShipping = False + self.AllowServiceDate = False + self.ETransactionEnabledStatus = "" # e.g. "NotApplicable" + self.DefaultCustomerMessage = "" # e.g. "Thank you for your business and have a great day!" + self.EmailCopyToCompany = False + self.AllowEstimates = True + self.DefaultTerms = None + self.AllowDiscount = True + self.DefaultDiscountAccount = "" + self.DefaultShippingAccount = False + self.AllowDeposit = True + self.AutoApplyPayments = True + self.IPNSupportEnabled = False + self.AutoApplyCredit = True + self.CustomField = None + self.UsingPriceLevels = False + self.ETransactionAttachPDF = False + self.UsingProgressInvoicing = False + self.EstimateMessage = "" + + self.DefaultTerms = None + self.CustomField = None + self.SalesEmailBcc = None + self.SalesEmailCc = None + + +class VendorAndPurchasesPrefs(QuickbooksBaseObject): + class_dict = { + "DefaultTerms": Ref, + "DefaultMarkupAccount": Ref + } + detail_dict = { + "POCustomField": PreferencesCustomFieldGroup + } + + def __init__(self): + super().__init__() + self.BillableExpenseTracking = True + self.TrackingByCustomer = True + self.TPAREnabled = True + + self.POCustomField = None + self.DefaultMarkupAccount = None + self.DefaultTerms = None + + +class TaxPrefs(QuickbooksBaseObject): + class_dict = { + "TaxGroupCodeRef": Ref + } + + def __init__(self): + super().__init__() + self.TaxGroupCodeRef = None + self.UsingSalesTax = True + self.PartnerTaxEnabled = True + + +class NameValue(QuickbooksBaseObject): + def __init__(self): + super().__init__() + self.Name = "" + self.Value = "" + + +class OtherPrefs(QuickbooksBaseObject): + list_dict = { + "NameValue": NameValue + } + + def __init__(self): + super().__init__() + self.NameValue = None + + +class TimeTrackingPrefs(QuickbooksBaseObject): + def __init__(self): + super().__init__() + self.WorkWeekStartDate = "" # e.g. "Monday" + self.MarkTimeEntriesBillable = True + self.ShowBillRateToAll = False + self.UseServices = True + self.BillCustomers = True + + +class CurrencyPrefs(QuickbooksBaseObject): + class_dict = { + "HomeCurrency": Ref + } + + def __init__(self): + super().__init__() + self.HomeCurrency = None + self.MultiCurrencyEnabled = False + + +class Preferences(PrefMixin, UpdateNoIdMixin, QuickbooksTransactionEntity): + """ + QBO definition: The Preferences resource represents a set of company preferences that + control application behavior in QuickBooks Online. + They are mostly exposed as read-only through the Preferences endpoint with only a very small subset of them + available as writable. Preferences are not necessarily honored when making requests via the QuickBooks API + because a lot of them control UI behavior in the application and may not be applicable for apps. + """ + + class_dict = { + 'EmailMessagesPrefs': EmailMessagesPrefs, + 'ProductAndServicesPrefs': ProductAndServicesPrefs, + 'ReportPrefs': ReportPrefs, + 'AccountingInfoPrefs': AccountingInfoPrefs, + 'SalesFormsPrefs': SalesFormsPrefs, + 'VendorAndPurchasesPrefs': VendorAndPurchasesPrefs, + 'TaxPrefs': TaxPrefs, + 'OtherPrefs': OtherPrefs, + 'TimeTrackingPrefs': TimeTrackingPrefs, + 'CurrencyPrefs': CurrencyPrefs, + } + + qbo_object_name = "Preferences" + + def __init__(self): + super().__init__() + self.EmailMessagesPrefs = None + self.ProductAndServicesPrefs = None + self.ReportPrefs = None + self.AccountingInfoPrefs = None + self.SalesFormsPrefs = None + self.VendorAndPurchasesPrefs = None + self.TaxPrefs = None + self.OtherPrefs = None + self.TimeTrackingPrefs = None + self.CurrencyPrefs = None + + def __str__(self): + return 'Preferences {0}'.format(self.Id) diff --git a/quickbooks/objects/purchase.py b/quickbooks/objects/purchase.py index 59d10251..62b2c722 100644 --- a/quickbooks/objects/purchase.py +++ b/quickbooks/objects/purchase.py @@ -1,5 +1,3 @@ -from six import python_2_unicode_compatible - from quickbooks.objects.detailline import DetailLine, AccountBasedExpenseLine, ItemBasedExpenseLine, \ TDSLine from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, \ @@ -8,7 +6,6 @@ from ..mixins import DeleteMixin -@python_2_unicode_compatible class Purchase(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: This entity represents expenses, such as a purchase made from a vendor. @@ -22,7 +19,7 @@ class Purchase(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity For example, to create a transaction that sends a check to a vendor, create a Purchase object with PaymentType set to Check. To query Purchase transactions of a certain type, for example Check, submit the following to the query endpoint: SELECT * from Purchase where PaymentType='Check' You must specify an AccountRef for all purchases. - The TotalAmtattribute must add up to sum of Line.Amount attributes. + The TotalAmt attribute must add up to sum of Line.Amount attributes. """ class_dict = { "AccountRef": Ref, diff --git a/quickbooks/objects/purchaseorder.py b/quickbooks/objects/purchaseorder.py index 6c6ac244..563391ea 100644 --- a/quickbooks/objects/purchaseorder.py +++ b/quickbooks/objects/purchaseorder.py @@ -1,5 +1,3 @@ -from six import python_2_unicode_compatible - from quickbooks.objects.detailline import DetailLine, ItemBasedExpenseLine, AccountBasedExpenseLine, \ TDSLine from .base import Ref, Address, QuickbooksManagedObject, LinkedTxnMixin, \ @@ -8,7 +6,6 @@ from ..mixins import DeleteMixin, SendMixin -@python_2_unicode_compatible class PurchaseOrder(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, SendMixin): """ QBO definition: The PurchaseOrder entity is a non-posting transaction representing a request to purchase diff --git a/quickbooks/objects/recurringtransaction.py b/quickbooks/objects/recurringtransaction.py new file mode 100644 index 00000000..2bfb3fe2 --- /dev/null +++ b/quickbooks/objects/recurringtransaction.py @@ -0,0 +1,130 @@ +from .bill import Bill +from .creditmemo import CreditMemo +from .deposit import Deposit +from .estimate import Estimate +from .invoice import Invoice +from .journalentry import JournalEntry +from .purchase import Purchase +from .purchaseorder import PurchaseOrder +from .refundreceipt import RefundReceipt +from .salesreceipt import SalesReceipt +from .transfer import Transfer +from .vendorcredit import VendorCredit +from .base import Ref, QuickbooksBaseObject +from ..mixins import UpdateNoIdMixin, ListMixin, ReadMixin, DeleteNoIdMixin + + +class ScheduleInfo(QuickbooksBaseObject): + def __init__(self): + super(ScheduleInfo, self).__init__() + + self.StartDate = None + self.EndDate = None + self.DaysBefore = None + self.MaxOccurrences = None + + self.RemindDays = None + self.IntervalType = None + self.NumInterval = None + + self.DayOfMonth = None + self.DayOfWeek = None + self.MonthOfYear = None + self.WeekOfMonth = None + + self.NextDate = None + self.PreviousDate = None + + +class RecurringInfo(QuickbooksBaseObject): + class_dict = { + "ScheduleInfo": ScheduleInfo + } + + qbo_object_name = "RecurringInfo" + + def __init__(self): + super(RecurringInfo, self).__init__() + + self.RecurType = "Automated" + self.Name = "" + self.Active = False + + +class Recurring(): + class_dict = { + "RecurringInfo": RecurringInfo, + "RecurDataRef": Ref + } + + +class RecurringBill(Bill): + class_dict = {**Bill.class_dict, **Recurring.class_dict} + + +class RecurringPurchase(Purchase): + class_dict = {**Purchase.class_dict, **Recurring.class_dict} + + +class RecurringCreditMemo(CreditMemo): + class_dict = {**CreditMemo.class_dict, **Recurring.class_dict} + + +class RecurringDeposit(Deposit): + class_dict = {**Deposit.class_dict, **Recurring.class_dict} + + +class RecurringEstimate(Estimate): + class_dict = {**Estimate.class_dict, **Recurring.class_dict} + + +class RecurringInvoice(Invoice): + class_dict = {**Invoice.class_dict, **Recurring.class_dict} + + +class RecurringJournalEntry(JournalEntry): + class_dict = {**JournalEntry.class_dict, **Recurring.class_dict} + + +class RecurringRefundReceipt(RefundReceipt): + class_dict = {**RefundReceipt.class_dict, **Recurring.class_dict} + + +class RecurringSalesReceipt(SalesReceipt): + class_dict = {**SalesReceipt.class_dict, **Recurring.class_dict} + + +class RecurringTransfer(Transfer): + class_dict = {**Transfer.class_dict, **Recurring.class_dict} + + +class RecurringVendorCredit(VendorCredit): + class_dict = {**VendorCredit.class_dict, **Recurring.class_dict} + + +class RecurringPurchaseOrder(PurchaseOrder): + class_dict = {**PurchaseOrder.class_dict, **Recurring.class_dict} + + +class RecurringTransaction(QuickbooksBaseObject, ReadMixin, UpdateNoIdMixin, ListMixin, DeleteNoIdMixin): + """ + QBO definition: A RecurringTransaction object refers to scheduling creation of transactions, + set up reminders and create transaction template for later use. + This feature is available in QuickBooks Essentials and Plus SKU. + """ + class_dict = { + "Bill": RecurringBill, + "Purchase": RecurringPurchase, + "CreditMemo": RecurringCreditMemo, + "Deposit": RecurringDeposit, + "Estimate": RecurringEstimate, + "Invoice": RecurringInvoice, + "JournalEntry": RecurringJournalEntry, + "RefundReceipt": RecurringRefundReceipt, + "SalesReceipt": RecurringSalesReceipt, + "Transfer": RecurringTransfer, + "VendorCredit": RecurringVendorCredit, + "PurchaseOrder": RecurringPurchaseOrder + } + + qbo_object_name = "RecurringTransaction" diff --git a/quickbooks/objects/refundreceipt.py b/quickbooks/objects/refundreceipt.py index ca45c654..fc56506e 100644 --- a/quickbooks/objects/refundreceipt.py +++ b/quickbooks/objects/refundreceipt.py @@ -1,5 +1,3 @@ -from six import python_2_unicode_compatible - from quickbooks.objects import CreditCardPayment from .base import Ref, CustomField, QuickbooksManagedObject, \ LinkedTxnMixin, QuickbooksTransactionEntity, LinkedTxn, Address, EmailAddress, QuickbooksBaseObject, CustomerMemo @@ -8,7 +6,6 @@ from ..mixins import DeleteMixin -@python_2_unicode_compatible class RefundReceiptCheckPayment(QuickbooksBaseObject): qbo_object_name = "CheckPayment" @@ -24,7 +21,6 @@ def __str__(self): return self.CheckNum -@python_2_unicode_compatible class RefundReceipt(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: RefundReceipt represents a refund to the customer for a product or service that was given. @@ -35,8 +31,8 @@ class RefundReceipt(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionE "TxnTaxDetail": TxnTaxDetail, "DepositToAccountRef": Ref, "CustomerRef": Ref, - "BillAddr": Address, - "ShipAddr": Address, + "BillAddr": Address, + "ShipAddr": Address, "ClassRef": Ref, "BillEmail": EmailAddress, "PaymentMethodRef": Ref, diff --git a/quickbooks/objects/salesreceipt.py b/quickbooks/objects/salesreceipt.py index 91ff18e8..0a42925b 100644 --- a/quickbooks/objects/salesreceipt.py +++ b/quickbooks/objects/salesreceipt.py @@ -1,14 +1,12 @@ -from six import python_2_unicode_compatible from .base import Ref, CustomField, QuickbooksManagedObject, LinkedTxnMixin, Address, \ EmailAddress, QuickbooksTransactionEntity, LinkedTxn from .tax import TxnTaxDetail from .detailline import DetailLine -from ..mixins import QuickbooksPdfDownloadable, DeleteMixin +from ..mixins import QuickbooksPdfDownloadable, DeleteMixin, VoidMixin -@python_2_unicode_compatible class SalesReceipt(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject, - QuickbooksTransactionEntity, LinkedTxnMixin): + QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin): """ QBO definition: SalesReceipt represents the sales receipt that is given to a customer. A sales receipt is similar to an invoice. However, for a sales receipt, payment is received diff --git a/quickbooks/objects/tax.py b/quickbooks/objects/tax.py index 295a789f..aa515071 100644 --- a/quickbooks/objects/tax.py +++ b/quickbooks/objects/tax.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject, Ref, QuickbooksManagedObject -@python_2_unicode_compatible class TaxLineDetail(QuickbooksBaseObject): class_dict = { "TaxRateRef": Ref @@ -18,7 +16,6 @@ def __str__(self): return str(self.TaxPercent) -@python_2_unicode_compatible class TaxLine(QuickbooksBaseObject): class_dict = { "TaxLineDetail": TaxLineDetail @@ -33,7 +30,6 @@ def __str__(self): return str(self.Amount) -@python_2_unicode_compatible class TxnTaxDetail(QuickbooksBaseObject): class_dict = { "TxnTaxCodeRef": Ref, diff --git a/quickbooks/objects/taxagency.py b/quickbooks/objects/taxagency.py index 85de9ad9..171e1d43 100644 --- a/quickbooks/objects/taxagency.py +++ b/quickbooks/objects/taxagency.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksTransactionEntity, QuickbooksManagedObject -@python_2_unicode_compatible class TaxAgency(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: Tax Agency is an entity that is associated with a tax rate and identifies the agency to which that tax rate diff --git a/quickbooks/objects/taxcode.py b/quickbooks/objects/taxcode.py index 8943dff3..aa720dcb 100644 --- a/quickbooks/objects/taxcode.py +++ b/quickbooks/objects/taxcode.py @@ -1,4 +1,3 @@ -from six import python_2_unicode_compatible from quickbooks.mixins import ListMixin, ReadMixin from .base import QuickbooksTransactionEntity, Ref, QuickbooksBaseObject @@ -29,7 +28,6 @@ def __init__(self): self.TaxRateDetail = [] -@python_2_unicode_compatible class TaxCode(QuickbooksTransactionEntity, QuickbooksBaseObject, ReadMixin, ListMixin): """ QBO definition: A TaxCode object is used to track the taxable or non-taxable status of products, diff --git a/quickbooks/objects/taxrate.py b/quickbooks/objects/taxrate.py index d172919a..af872f81 100644 --- a/quickbooks/objects/taxrate.py +++ b/quickbooks/objects/taxrate.py @@ -1,10 +1,7 @@ -from six import python_2_unicode_compatible - from quickbooks.mixins import ListMixin, ReadMixin from .base import QuickbooksTransactionEntity, Ref, QuickbooksBaseObject -@python_2_unicode_compatible class TaxRate(QuickbooksTransactionEntity, QuickbooksBaseObject, ReadMixin, ListMixin): """ QBO definition: A TaxRate object represents rate applied to calculate tax liability. Use the TaxService @@ -34,5 +31,3 @@ def __init__(self): def __str__(self): return self.Name - - diff --git a/quickbooks/objects/taxservice.py b/quickbooks/objects/taxservice.py index 84819b85..870d9065 100644 --- a/quickbooks/objects/taxservice.py +++ b/quickbooks/objects/taxservice.py @@ -1,10 +1,8 @@ -from six import python_2_unicode_compatible from .base import QuickbooksBaseObject from ..mixins import UpdateMixin from ..client import QuickBooks -@python_2_unicode_compatible class TaxRateDetails(QuickbooksBaseObject): qbo_object_name = "TaxRateDetails" @@ -20,7 +18,6 @@ def __str__(self): return self.TaxRateName -@python_2_unicode_compatible class TaxService(QuickbooksBaseObject, UpdateMixin): """ QBO definition: The TaxService endpoint allows you to perform the following actions: diff --git a/quickbooks/objects/term.py b/quickbooks/objects/term.py index 26466eb5..674ac98e 100644 --- a/quickbooks/objects/term.py +++ b/quickbooks/objects/term.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksManagedObject, QuickbooksTransactionEntity, Ref -@python_2_unicode_compatible class Term(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: The Term entity represents the terms under which a sale is made, typically expressed in the @@ -38,4 +36,3 @@ def to_ref(self): ref.value = self.Id return ref - diff --git a/quickbooks/objects/timeactivity.py b/quickbooks/objects/timeactivity.py index 132471e6..1d4bde48 100644 --- a/quickbooks/objects/timeactivity.py +++ b/quickbooks/objects/timeactivity.py @@ -1,9 +1,7 @@ -from six import python_2_unicode_compatible from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, AttachableRef from ..mixins import DeleteMixin -@python_2_unicode_compatible class TimeActivity(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: The TimeActivity entity represents a record of time worked by a vendor or employee. @@ -34,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/quickbooks/objects/trackingclass.py b/quickbooks/objects/trackingclass.py index cd47cd70..da084bd7 100644 --- a/quickbooks/objects/trackingclass.py +++ b/quickbooks/objects/trackingclass.py @@ -1,8 +1,6 @@ -from six import python_2_unicode_compatible from .base import QuickbooksManagedObject, QuickbooksTransactionEntity, Ref -@python_2_unicode_compatible class Class(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: Classes provide a way to track different segments of the business so they're diff --git a/quickbooks/objects/transfer.py b/quickbooks/objects/transfer.py index 42a593e6..d4954353 100644 --- a/quickbooks/objects/transfer.py +++ b/quickbooks/objects/transfer.py @@ -1,9 +1,7 @@ -from six import python_2_unicode_compatible from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin from ..mixins import DeleteMixin -@python_2_unicode_compatible class Transfer(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: A Transfer represents a transaction where funds are moved between two accounts from the diff --git a/quickbooks/objects/vendor.py b/quickbooks/objects/vendor.py index 03812e53..dec0cfde 100644 --- a/quickbooks/objects/vendor.py +++ b/quickbooks/objects/vendor.py @@ -1,4 +1,3 @@ -from six import python_2_unicode_compatible from .base import Address, PhoneNumber, EmailAddress, WebAddress, Ref, QuickbooksBaseObject, \ QuickbooksManagedObject, QuickbooksTransactionEntity @@ -15,7 +14,6 @@ def __init__(self): self.Telephone = None -@python_2_unicode_compatible class Vendor(QuickbooksManagedObject, QuickbooksTransactionEntity): """ QBO definition: The Vendor represents the seller from whom your company purchases any service or product. @@ -49,8 +47,9 @@ def __init__(self): self.Active = True self.TaxIdentifier = "" self.Balance = 0 + self.BillRate = 0 self.AcctNum = "" - self.Vendor1099 = True + self.Vendor1099 = False self.TaxReportingBasis = "" self.BillAddr = None diff --git a/quickbooks/objects/vendorcredit.py b/quickbooks/objects/vendorcredit.py index 53d8abd9..3f57d508 100644 --- a/quickbooks/objects/vendorcredit.py +++ b/quickbooks/objects/vendorcredit.py @@ -1,12 +1,9 @@ -from six import python_2_unicode_compatible - from .base import Ref, QuickbooksManagedObject, QuickbooksTransactionEntity, \ LinkedTxnMixin from .detailline import DetailLine, AccountBasedExpenseLine, ItemBasedExpenseLine, TDSLine from ..mixins import DeleteMixin -@python_2_unicode_compatible class VendorCredit(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin): """ QBO definition: The Vendor Credit entity is an accounts payable transaction that represents a refund or credit @@ -47,4 +44,4 @@ def __init__(self): self.Line = [] def __str__(self): - return str(self.TotalAmt) \ No newline at end of file + return str(self.TotalAmt) diff --git a/quickbooks/utils.py b/quickbooks/utils.py index 91f39d84..5387e950 100644 --- a/quickbooks/utils.py +++ b/quickbooks/utils.py @@ -1,7 +1,3 @@ -import six -import sys - - def build_where_clause(**kwargs): where_clause = "" @@ -9,11 +5,7 @@ def build_where_clause(**kwargs): where = [] for key, value in kwargs.items(): - if isinstance(value, six.text_type) and sys.version_info[0] == 2: - # If using python 2, encode unicode as string. - encoded_value = value.encode('utf-8') - where.append("{0} = '{1}'".format(key, encoded_value.replace(r"'", r"\'"))) - elif isinstance(value, six.string_types): + if isinstance(value, str): where.append("{0} = '{1}'".format(key, value.replace(r"'", r"\'"))) else: where.append("{0} = {1}".format(key, value)) @@ -30,11 +22,7 @@ def build_choose_clause(choices, field): where = [] for choice in choices: - if isinstance(choice, six.text_type) and sys.version_info[0] == 2: - # If using python 2, encode unicode as string. - encoded_choice = choice.encode('utf-8') - where.append("'{0}'".format(encoded_choice.replace(r"'", r"\'"))) - elif isinstance(choice, six.string_types): + if isinstance(choice, str): where.append("'{0}'".format(choice.replace(r"'", r"\'"))) else: where.append("{0}".format(choice)) diff --git a/requirements.txt b/requirements.txt index e6368351..401d5890 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -intuit-oauth==1.2.3 -rauth>=0.7.1 -requests>=2.19.1 -simplejson>=3.17.0 -six>=1.14.0 +intuit-oauth==1.2.6 +requests_oauthlib>=1.3.1 +requests>=2.31.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 493746f0..5af119fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,18 @@ -[metadata] -description-file = README.md +[project] +name = "python-quickbooks" +version = "0.9.12" [flake8] -max-line-length = 100 -max-complexity = 10 +max_line_length = 100 +max_complexity = 10 filename = *.py format = default exclude =/quickbooks/objects/__init__.py + +[coverage:run] +branch = True +omit = src/db/env.py,src/db/versions/* # define paths to omit + +[coverage:report] +show_missing = True +skip_covered = True diff --git a/setup.py b/setup.py index 92d0d9ae..b9d6fb9c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def read(*parts): return fp.read() -VERSION = (0, 8, 4) +VERSION = (0, 9, 12) version = '.'.join(map(str, VERSION)) setup( @@ -30,15 +30,10 @@ def read(*parts): }, install_requires=[ - 'setuptools', - 'intuit-oauth==1.2.3', - 'rauth>=0.7.1', - 'authclient', - 'requests>=2.19.1', - 'simplejson>=3.17.0', - 'six>=1.14.0', + 'intuit-oauth==1.2.6', + 'requests_oauthlib>=1.3.1', + 'requests>=2.31.0', 'python-dateutil', - 'pycparser==2.18' ], classifiers=[ @@ -51,6 +46,10 @@ def read(*parts): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], - packages=find_packages(), + packages=find_packages(exclude=("tests",)), ) diff --git a/tests/integration/test_account.py b/tests/integration/test_account.py index 7c69d339..741a6b7a 100644 --- a/tests/integration/test_account.py +++ b/tests/integration/test_account.py @@ -7,28 +7,83 @@ class AccountTest(QuickbooksTestCase): def setUp(self): super(AccountTest, self).setUp() + self.time = datetime.now() self.account_number = datetime.now().strftime('%d%H%M') self.name = "Test Account {0}".format(self.account_number) 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.assertEquals(account.Id, query_account.Id) - self.assertEquals(query_account.Name, self.name) - self.assertEquals(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) + + # Change the name + updated_name = f"{unique_name}_updated" + created_account.Name = updated_name + updated_account = created_account.save(qb=self.qb_client) + + # 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": unique_number, + "Name": unique_name, + "AccountType": "Bank", + "AccountSubType": "CashOnHand" + }) - account.Name = "Updated Name {0}".format(self.account_number) - account.save(qb=self.qb_client) + created_account = account.save(qb=self.qb_client) + self.assertIsNotNone(created_account) + self.assertIsNotNone(created_account.Id) - query_account = Account.get(account.Id, qb=self.qb_client) - self.assertEquals(query_account.Name, "Updated Name {0}".format(self.account_number)) + # 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 e690acfe..2639bb10 100644 --- a/tests/integration/test_attachable.py +++ b/tests/integration/test_attachable.py @@ -27,8 +27,8 @@ def test_create_note(self): attachable.save(qb=self.qb_client) query_attachable = Attachable.get(attachable.Id, qb=self.qb_client) - self.assertEquals(query_attachable.AttachableRef[0].EntityRef.value, vendor.Id) - self.assertEquals(query_attachable.Note, "Test note added on {}".format(self.time.strftime("%Y-%m-%d %H:%M:%S"))) + self.assertEqual(query_attachable.AttachableRef[0].EntityRef.value, vendor.Id) + self.assertEqual(query_attachable.Note, "Test note added on {}".format(self.time.strftime("%Y-%m-%d %H:%M:%S"))) def test_update_note(self): attachable = Attachable.all(max_results=1, qb=self.qb_client)[0] @@ -37,11 +37,15 @@ def test_update_note(self): attachable.save(qb=self.qb_client) query_attachable = Attachable.get(attachable.Id, qb=self.qb_client) - self.assertEquals(query_attachable.Note, "Note updated on {}".format(self.time.strftime("%Y-%m-%d %H:%M:%S"))) + self.assertEqual(query_attachable.Note, "Note updated on {}".format(self.time.strftime("%Y-%m-%d %H:%M:%S"))) def test_create_file(self): attachable = Attachable() - test_file = tempfile.NamedTemporaryFile(suffix=".txt") + test_file = tempfile.NamedTemporaryFile(mode='w+t', suffix=".txt", delete=False) + + with test_file as f: + f.write("File contents") + f.flush() vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] @@ -49,12 +53,41 @@ def test_create_file(self): attachable_ref.EntityRef = vendor.to_ref() attachable.AttachableRef.append(attachable_ref) + attachable.Note = "Sample note" attachable.FileName = os.path.basename(test_file.name) attachable._FilePath = test_file.name attachable.ContentType = 'text/plain' attachable.save(qb=self.qb_client) + test_file.close() + os.unlink(test_file.name) + query_attachable = Attachable.get(attachable.Id, qb=self.qb_client) - self.assertEquals(query_attachable.AttachableRef[0].EntityRef.value, vendor.Id) + 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 4eda02fc..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=54, 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 @@ -33,14 +33,15 @@ def setUp(self): self.auth_client = AuthClient( client_id='CLIENTID', client_secret='CLIENT_SECRET', - environment=Environments.SANDBOX, + environment='sandbox', redirect_uri='http://localhost:8000/callback', ) self.qb_client = QuickBooks( - #auth_client=self.auth_client, + # 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_bill.py b/tests/integration/test_bill.py index 631b4474..324ac6ce 100644 --- a/tests/integration/test_bill.py +++ b/tests/integration/test_bill.py @@ -1,6 +1,6 @@ from datetime import datetime -from quickbooks.objects.base import Ref +from quickbooks.objects.base import Ref, Address from quickbooks.objects.bill import Bill from quickbooks.objects.detailline import AccountBasedExpenseLine, AccountBasedExpenseLineDetail from quickbooks.objects.vendor import Vendor @@ -30,10 +30,29 @@ def test_create(self): vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] bill.VendorRef = vendor.to_ref() + # Test undocumented VendorAddr field + bill.VendorAddr = Address() + bill.VendorAddr.Line1 = "123 Main" + bill.VendorAddr.Line2 = "Apartment 1" + bill.VendorAddr.City = "City" + bill.VendorAddr.Country = "U.S.A" + bill.VendorAddr.CountrySubDivisionCode = "CA" + bill.VendorAddr.PostalCode = "94030" + bill.save(qb=self.qb_client) query_bill = Bill.get(bill.Id, qb=self.qb_client) - self.assertEquals(query_bill.Id, bill.Id) - self.assertEquals(len(query_bill.Line), 1) - self.assertEquals(query_bill.Line[0].Amount, 200.0) + self.assertEqual(query_bill.Id, bill.Id) + self.assertEqual(len(query_bill.Line), 1) + self.assertEqual(query_bill.Line[0].Amount, 200.0) + + self.assertEqual(query_bill.VendorAddr.Line1, bill.VendorAddr.Line1) + self.assertEqual(query_bill.VendorAddr.Line2, bill.VendorAddr.Line2) + self.assertEqual(query_bill.VendorAddr.City, bill.VendorAddr.City) + self.assertEqual(query_bill.VendorAddr.Country, bill.VendorAddr.Country) + self.assertEqual(query_bill.VendorAddr.CountrySubDivisionCode, bill.VendorAddr.CountrySubDivisionCode) + self.assertEqual(query_bill.VendorAddr.PostalCode, bill.VendorAddr.PostalCode) + + + diff --git a/tests/integration/test_billpayment.py b/tests/integration/test_billpayment.py index 0d300493..c7ce6501 100644 --- a/tests/integration/test_billpayment.py +++ b/tests/integration/test_billpayment.py @@ -1,5 +1,6 @@ from datetime import datetime +from quickbooks.objects import AccountBasedExpenseLine, Ref, AccountBasedExpenseLineDetail from quickbooks.objects.account import Account from quickbooks.objects.bill import Bill from quickbooks.objects.billpayment import BillPayment, BillPaymentLine, CheckPayment @@ -14,12 +15,30 @@ def setUp(self): self.account_number = datetime.now().strftime('%d%H%M') self.name = "Test Account {0}".format(self.account_number) - def test_create(self): + def create_bill(self, amount): + bill = Bill() + line = AccountBasedExpenseLine() + line.Amount = amount + line.DetailType = "AccountBasedExpenseLineDetail" + + account_ref = Ref() + account_ref.type = "Account" + account_ref.value = 1 + line.AccountBasedExpenseLineDetail = AccountBasedExpenseLineDetail() + line.AccountBasedExpenseLineDetail.AccountRef = account_ref + bill.Line.append(line) + + vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] + bill.VendorRef = vendor.to_ref() + + return bill.save(qb=self.qb_client) + + def create_bill_payment(self, bill, amount, private_note, pay_type): bill_payment = BillPayment() - bill_payment.PayType = "Check" - bill_payment.TotalAmt = 200 - bill_payment.PrivateNote = "Private Note" + bill_payment.PayType = pay_type + bill_payment.TotalAmt = amount + bill_payment.PrivateNote = private_note vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] bill_payment.VendorRef = vendor.to_ref() @@ -31,20 +50,37 @@ def test_create(self): ap_account = Account.where("AccountSubType = 'AccountsPayable'", qb=self.qb_client)[0] bill_payment.APAccountRef = ap_account.to_ref() - bill = Bill.all(max_results=1, qb=self.qb_client)[0] - line = BillPaymentLine() line.LinkedTxn.append(bill.to_linked_txn()) line.Amount = 200 bill_payment.Line.append(line) - bill_payment.save(qb=self.qb_client) + return bill_payment.save(qb=self.qb_client) + + def test_create(self): + # create new bill for testing, reusing the same bill will cause Line to be empty + # and the new bill payment will be voided automatically + bill = self.create_bill(amount=200) + bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check") query_bill_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client) - self.assertEquals(query_bill_payment.PayType, "Check") - self.assertEquals(query_bill_payment.TotalAmt, 200.0) - self.assertEquals(query_bill_payment.PrivateNote, "Private Note") + self.assertEqual(query_bill_payment.PayType, "Check") + self.assertEqual(query_bill_payment.TotalAmt, 200.0) + self.assertEqual(query_bill_payment.PrivateNote, "Private Note") + + self.assertEqual(len(query_bill_payment.Line), 1) + self.assertEqual(query_bill_payment.Line[0].Amount, 200.0) + + def test_void(self): + bill = self.create_bill(amount=200) + bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check") + query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client) + self.assertEqual(query_payment.TotalAmt, 200.0) + self.assertNotIn('Voided', query_payment.PrivateNote) + + bill_payment.void(qb=self.qb_client) + query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client) - self.assertEquals(len(query_bill_payment.Line), 1) - self.assertEquals(query_bill_payment.Line[0].Amount, 200.0) + self.assertEqual(query_payment.TotalAmt, 0.0) + self.assertIn('Voided', query_payment.PrivateNote) \ No newline at end of file diff --git a/tests/integration/test_creditcardpayment_entity.py b/tests/integration/test_creditcardpayment_entity.py index 13c49ddf..245ff81c 100644 --- a/tests/integration/test_creditcardpayment_entity.py +++ b/tests/integration/test_creditcardpayment_entity.py @@ -38,10 +38,10 @@ def test_create(self): query_credit_card_payment = CreditCardPayment.get(credit_card_payment.Id, qb=self.qb_client) - self.assertEquals(query_credit_card_payment.Id, credit_card_payment.Id) - self.assertEquals(query_credit_card_payment.Amount, 100) - self.assertEquals(query_credit_card_payment.BankAccountRef.value, from_account.Id) - self.assertEquals(query_credit_card_payment.CreditCardAccountRef.value, to_account.Id) + self.assertEqual(query_credit_card_payment.Id, credit_card_payment.Id) + self.assertEqual(query_credit_card_payment.Amount, 100) + self.assertEqual(query_credit_card_payment.BankAccountRef.value, from_account.Id) + self.assertEqual(query_credit_card_payment.CreditCardAccountRef.value, to_account.Id) # reset transfer (so the from_account doesn't run out of cash) # I wonder if we can do a transfer from credit_card_account to a bank_account @@ -59,4 +59,4 @@ def test_update(self): query_credit_card_payment = CreditCardPayment.get(credit_card_payment.Id, qb=self.qb_client) - self.assertEquals(query_credit_card_payment.Amount, credit_card_payment.Amount) + self.assertEqual(query_credit_card_payment.Amount, credit_card_payment.Amount) diff --git a/tests/integration/test_creditmemo.py b/tests/integration/test_creditmemo.py index 7505688b..d86df5d0 100644 --- a/tests/integration/test_creditmemo.py +++ b/tests/integration/test_creditmemo.py @@ -26,15 +26,15 @@ def test_create(self): query_credit_memo = CreditMemo.get(credit_memo.Id, qb=self.qb_client) - self.assertEquals(credit_memo.Id, query_credit_memo.Id) - self.assertEquals(query_credit_memo.CustomerRef.value, customer.Id) + self.assertEqual(credit_memo.Id, query_credit_memo.Id) + self.assertEqual(query_credit_memo.CustomerRef.value, customer.Id) line = query_credit_memo.Line[0] - self.assertEquals(line.LineNum, 1) - self.assertEquals(line.Description, "Test Description") - self.assertEquals(line.Amount, 100) - self.assertEquals(line.DetailType, "SalesItemLineDetail") - self.assertEquals(line.SalesItemLineDetail.ItemRef.value, item.Id) + self.assertEqual(line.LineNum, 1) + self.assertEqual(line.Description, "Test Description") + self.assertEqual(line.Amount, 100) + self.assertEqual(line.DetailType, "SalesItemLineDetail") + self.assertEqual(line.SalesItemLineDetail.ItemRef.value, item.Id) def test_update(self): credit_memo = CreditMemo.all(max_results=1, qb=self.qb_client)[0] @@ -42,6 +42,6 @@ def test_update(self): credit_memo.save(qb=self.qb_client) query_credit_memo = CreditMemo.get(credit_memo.Id, qb=self.qb_client) - self.assertEquals(query_credit_memo.PrivateNote, "Test") + self.assertEqual(query_credit_memo.PrivateNote, "Test") diff --git a/tests/integration/test_customer.py b/tests/integration/test_customer.py index fbbd4a8f..c1781c46 100644 --- a/tests/integration/test_customer.py +++ b/tests/integration/test_customer.py @@ -47,7 +47,7 @@ def test_create(self): query_customer = Customer.get(customer.Id, qb=self.qb_client) - self.assertEquals(customer.Id, query_customer.Id) + self.assertEqual(customer.Id, query_customer.Id) self.assertEqual(query_customer.Title, self.title) self.assertEqual(query_customer.GivenName, self.given_name) self.assertEqual(query_customer.MiddleName, self.middle_name) 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_estimate.py b/tests/integration/test_estimate.py index df9ffbe5..c9210149 100644 --- a/tests/integration/test_estimate.py +++ b/tests/integration/test_estimate.py @@ -93,6 +93,7 @@ def test_create(self): line2.DetailType = "DiscountLineDetail" estimate.Line.append(line2) + estimate.TrackingNum = "42" estimate.save(qb=self.qb_client) @@ -134,3 +135,4 @@ def test_create(self): estimate.Line[1].DiscountLineDetail.DiscountAccountRef.value) self.assertEqual(query_estimate.Line[2].DiscountLineDetail.DiscountAccountRef.name, estimate.Line[1].DiscountLineDetail.DiscountAccountRef.name) + self.assertEqual(query_estimate.TrackingNum, estimate.TrackingNum) \ No newline at end of file diff --git a/tests/integration/test_exchangerate.py b/tests/integration/test_exchangerate.py new file mode 100644 index 00000000..bc563a7c --- /dev/null +++ b/tests/integration/test_exchangerate.py @@ -0,0 +1,21 @@ +from datetime import datetime +from quickbooks.objects.exchangerate import ExchangeRate +from tests.integration.test_base import QuickbooksTestCase + + +class ExchangeRateTest(QuickbooksTestCase): + def test_query(self): + exchange_rate = ExchangeRate.where("SourceCurrencyCode = 'EUR'", qb=self.qb_client)[0] + + self.assertEqual(exchange_rate.SourceCurrencyCode, "EUR") + self.assertEqual(exchange_rate.TargetCurrencyCode, "USD") + + def test_update(self): + exchange_rate = ExchangeRate.where("SourceCurrencyCode = 'EUR'", qb=self.qb_client)[0] + + new_rate = exchange_rate.Rate + 1 + exchange_rate.Rate = new_rate + exchange_rate.save(qb=self.qb_client) + + exchange_rate_updated = ExchangeRate.where("SourceCurrencyCode = 'EUR'", qb=self.qb_client)[0] + self.assertEqual(exchange_rate_updated.Rate, new_rate) diff --git a/tests/integration/test_invoice.py b/tests/integration/test_invoice.py index e0b5a197..8b93f1da 100644 --- a/tests/integration/test_invoice.py +++ b/tests/integration/test_invoice.py @@ -1,15 +1,17 @@ +from datetime import datetime + from quickbooks.objects.base import CustomerMemo from quickbooks.objects.customer import Customer from quickbooks.objects.detailline import SalesItemLine, SalesItemLineDetail from quickbooks.objects.invoice import Invoice from quickbooks.objects.item import Item +from quickbooks.objects.base import EmailAddress from tests.integration.test_base import QuickbooksTestCase +import uuid class InvoiceTest(QuickbooksTestCase): - def test_create(self): - invoice = Invoice() - + def create_invoice_line(self): line = SalesItemLine() line.LineNum = 1 line.Description = "description" @@ -18,42 +20,60 @@ def test_create(self): item = Item.all(max_results=1, qb=self.qb_client)[0] line.SalesItemLineDetail.ItemRef = item.to_ref() - invoice.Line.append(line) + return line + + def create_invoice(self, customer, request_id=None): + invoice = Invoice() + invoice.Line.append(self.create_invoice_line()) - customer = Customer.all(max_results=1, qb=self.qb_client)[0] invoice.CustomerRef = customer.to_ref() invoice.CustomerMemo = CustomerMemo() invoice.CustomerMemo.value = "Customer Memo" - invoice.save(qb=self.qb_client) + invoice.save(qb=self.qb_client, request_id=request_id) + return invoice + + def test_query_by_customer_ref(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + invoice = Invoice.query( + "select * from Invoice where CustomerRef = '{0}'".format(customer.Id), qb=self.qb_client) - query_invoice = Invoice.get(invoice.Id, qb=self.qb_client) + print(invoice[0].Line[0].LineNum) + print(invoice[0].Line[0].Amount) + self.assertEqual(invoice[0].CustomerRef.name, customer.DisplayName) - self.assertEquals(query_invoice.CustomerRef.name, customer.DisplayName) - self.assertEquals(query_invoice.CustomerMemo.value, "Customer Memo") - self.assertEquals(query_invoice.Line[0].Description, "description") - self.assertEquals(query_invoice.Line[0].Amount, 100.0) + def test_where(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] - def test_delete(self): - # First create an invoice - invoice = Invoice() + invoice = Invoice.where( + "CustomerRef = '{0}'".format(customer.Id), qb=self.qb_client) - line = SalesItemLine() - line.LineNum = 1 - line.Description = "description" - line.Amount = 100 - line.SalesItemLineDetail = SalesItemLineDetail() - item = Item.all(max_results=1, qb=self.qb_client)[0] + print(invoice[0]) + self.assertEqual(invoice[0].CustomerRef.name, customer.DisplayName) - line.SalesItemLineDetail.ItemRef = item.to_ref() - invoice.Line.append(line) + def test_create(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + invoice = self.create_invoice(customer) + query_invoice = Invoice.get(invoice.Id, qb=self.qb_client) + self.assertEqual(query_invoice.CustomerRef.name, customer.DisplayName) + self.assertEqual(query_invoice.CustomerMemo.value, "Customer Memo") + self.assertEqual(query_invoice.Line[0].Description, "description") + self.assertEqual(query_invoice.Line[0].Amount, 100.0) + + def test_create_idempotence(self): customer = Customer.all(max_results=1, qb=self.qb_client)[0] - invoice.CustomerRef = customer.to_ref() + sample_request_id = str(uuid.uuid4()) + invoice = self.create_invoice(customer, request_id=sample_request_id) + duplicate_invoice = self.create_invoice(customer, request_id=sample_request_id) - invoice.CustomerMemo = CustomerMemo() - invoice.CustomerMemo.value = "Customer Memo" - invoice.save(qb=self.qb_client) + # Assert that both returned invoices have the same id + self.assertEqual(invoice.Id, duplicate_invoice.Id) + + def test_delete(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + # First create an invoice + invoice = self.create_invoice(customer) # Then delete invoice_id = invoice.Id @@ -61,3 +81,45 @@ def test_delete(self): query_invoice = Invoice.filter(Id=invoice_id, qb=self.qb_client) self.assertEqual([], query_invoice) + + def test_void(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + invoice = self.create_invoice(customer) + invoice_id = invoice.Id + invoice.void(qb=self.qb_client) + + query_invoice = Invoice.get(invoice_id, qb=self.qb_client) + self.assertEqual(query_invoice.Balance, 0.0) + self.assertEqual(query_invoice.TotalAmt, 0.0) + self.assertIn('Voided', query_invoice.PrivateNote) + + def test_invoice_link(self): + # Sharable link for the invoice sent to external customers. + # The link is generated only for invoices with online payment enabled and having a valid customer email address. + # Include query param `include=invoiceLink` to get the link back on query response. + + # Create test customer + customer_name = datetime.now().strftime('%d%H%M%S') + customer = Customer() + customer.DisplayName = customer_name + customer.save(qb=self.qb_client) + + # Create an invoice with sharable link flags set + invoice = Invoice() + invoice.CustomerRef = customer.to_ref() + invoice.DueDate = '2024-12-31' + invoice.AllowOnlineCreditCardPayment = True + invoice.AllowOnlineACHPayment = True + invoice.Line.append(self.create_invoice_line()) + + # BillEmail must be set for Sharable link to work! + invoice.BillEmail = EmailAddress() + invoice.BillEmail.Address = 'test@email.com' + + invoice.save(qb=self.qb_client) + + # You must add 'include': 'invoiceLink' to the params when doing a query for the invoice + query_invoice = Invoice.get(invoice.Id, qb=self.qb_client, params={'include': 'invoiceLink'}) + + self.assertIsNotNone(query_invoice.InvoiceLink) + self.assertIn('https', query_invoice.InvoiceLink) diff --git a/tests/integration/test_item.py b/tests/integration/test_item.py index fe8a80ff..f8ae4dcd 100644 --- a/tests/integration/test_item.py +++ b/tests/integration/test_item.py @@ -36,12 +36,31 @@ def test_create(self): query_item = Item.get(item.Id, qb=self.qb_client) - self.assertEquals(query_item.Id, item.Id) - self.assertEquals(query_item.Name, self.name) - self.assertEquals(query_item.Type, "Inventory") - self.assertEquals(query_item.Sku, "SKU123123") - self.assertEquals(query_item.TrackQtyOnHand, True) - self.assertEquals(query_item.QtyOnHand, 10) - self.assertEquals(query_item.IncomeAccountRef.value, self.income_account.Id) - self.assertEquals(query_item.ExpenseAccountRef.value, self.expense_account.Id) - self.assertEquals(query_item.AssetAccountRef.value, self.asset_account.Id) + self.assertEqual(query_item.Id, item.Id) + self.assertEqual(query_item.Name, self.name) + self.assertEqual(query_item.Type, "Inventory") + self.assertEqual(query_item.Sku, "SKU123123") + self.assertEqual(query_item.TrackQtyOnHand, True) + self.assertEqual(query_item.QtyOnHand, 10) + 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_payment.py b/tests/integration/test_payment.py index 19d3c9fb..24ff2e5b 100644 --- a/tests/integration/test_payment.py +++ b/tests/integration/test_payment.py @@ -37,3 +37,24 @@ def test_create(self): self.assertEqual(query_payment.CustomerRef.name, customer.DisplayName) self.assertEqual(query_payment.TotalAmt, 140.0) self.assertEqual(query_payment.PaymentMethodRef.value, payment_method.Id) + + def test_void(self): + payment = Payment() + payment.TotalAmt = 100.0 + + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + payment.CustomerRef = customer.to_ref() + + payment_method = PaymentMethod.all(max_results=1, qb=self.qb_client)[0] + + payment.PaymentMethodRef = payment_method.to_ref() + payment.save(qb=self.qb_client) + + query_payment = Payment.get(payment.Id, qb=self.qb_client) + self.assertEqual(query_payment.TotalAmt, 100.0) + + payment.void(qb=self.qb_client) + query_payment = Payment.get(payment.Id, qb=self.qb_client) + + self.assertEqual(query_payment.TotalAmt, 0.0) + self.assertIn('Voided', query_payment.PrivateNote) diff --git a/tests/integration/test_preferences.py b/tests/integration/test_preferences.py new file mode 100644 index 00000000..8e41c0d9 --- /dev/null +++ b/tests/integration/test_preferences.py @@ -0,0 +1,31 @@ +from datetime import datetime +from quickbooks.objects.preferences import Preferences +from tests.integration.test_base import QuickbooksTestCase + + +class PreferencesTest(QuickbooksTestCase): + def setUp(self): + super(PreferencesTest, self).setUp() + + self.account_number = datetime.now().strftime('%d%H%M') + self.name = "Test Account {0}".format(self.account_number) + + def test_get(self): + preferences = Preferences.get(qb=self.qb_client) + + self.assertEqual(preferences.Id, "1") + self.assertEqual(preferences.AccountingInfoPrefs.TaxYearMonth, "January") + self.assertEqual(preferences.ProductAndServicesPrefs.ForPurchase, True) + self.assertEqual(preferences.VendorAndPurchasesPrefs.BillableExpenseTracking, True) + self.assertEqual(preferences.TimeTrackingPrefs.WorkWeekStartDate, "Monday") + self.assertEqual(preferences.OtherPrefs.NameValue[0].Name, "SalesFormsPrefs.DefaultCustomerMessage") + + def test_update(self): + preferences = Preferences.get(qb=self.qb_client) + + subject = datetime.now().strftime('%d%H%M%S') + preferences.EmailMessagesPrefs.EstimateMessage.Subject = subject + preferences.save(qb=self.qb_client) + + preferences_updated = Preferences.get(qb=self.qb_client) + self.assertEqual(preferences_updated.EmailMessagesPrefs.EstimateMessage.Subject, subject) diff --git a/tests/integration/test_purchase.py b/tests/integration/test_purchase.py new file mode 100644 index 00000000..3c9c6e03 --- /dev/null +++ b/tests/integration/test_purchase.py @@ -0,0 +1,56 @@ +from quickbooks.objects.account import Account +from quickbooks.objects.customer import Customer +from quickbooks.objects.detailline import ItemBasedExpenseLine, ItemBasedExpenseLineDetail +from quickbooks.objects.item import Item +from quickbooks.objects.purchase import Purchase +from quickbooks.objects.taxcode import TaxCode +from tests.integration.test_base import QuickbooksTestCase + + +class PurchaseOrderTest(QuickbooksTestCase): + def test_create(self): + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + taxcode = TaxCode.all(max_results=1, qb=self.qb_client)[0] + item = Item.filter(Type='Inventory', max_results=1, qb=self.qb_client)[0] + + credit_account = Account() + credit_account.FullyQualifiedName = 'Visa' + credit_account.Id = "42" + + purchase = Purchase() + purchase.DocNumber = "Doc123" + purchase.PaymentType = "CreditCard" + purchase.AccountRef = credit_account.to_ref() + purchase.TotalAmt = 100 + + detail_line = ItemBasedExpenseLine() + detail_line.Amount = 100 + detail_line.ItemBasedExpenseLineDetail = ItemBasedExpenseLineDetail() + detail_line.ItemBasedExpenseLineDetail.BillableStatus = "NotBillable" + detail_line.ItemBasedExpenseLineDetail.UnitPrice = 100 + detail_line.ItemBasedExpenseLineDetail.Qty = 1 + detail_line.ItemBasedExpenseLineDetail.CustomerRef = customer.to_ref() + detail_line.ItemBasedExpenseLineDetail.TaxCodeRef = taxcode.to_ref() + detail_line.ItemBasedExpenseLineDetail.ItemRef = item.to_ref() + + purchase.Line.append(detail_line) + + print(purchase.to_json()) + purchase.save(qb=self.qb_client, params={'include': 'allowduplicatedocnum'}) + + query_purchase = Purchase.get(purchase.Id, qb=self.qb_client) + + self.assertEqual(query_purchase.AccountRef.value, credit_account.Id) + self.assertEqual(query_purchase.DocNumber, "Doc123") + self.assertEqual(query_purchase.TotalAmt, 100) + + query_detail_line = query_purchase.Line[0] + + self.assertEqual(query_detail_line.Amount, 100) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.UnitPrice, 100) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.Qty, 1) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.CustomerRef.value, customer.Id) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.TaxCodeRef.value, taxcode.Name) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.ItemRef.value, item.Id) + + diff --git a/tests/integration/test_purchaseorder.py b/tests/integration/test_purchaseorder.py index 6f4bbb55..bd9b3657 100644 --- a/tests/integration/test_purchaseorder.py +++ b/tests/integration/test_purchaseorder.py @@ -38,17 +38,17 @@ def test_create(self): query_purchaseorder = PurchaseOrder.get(purchaseorder.Id, qb=self.qb_client) - self.assertEquals(query_purchaseorder.VendorRef.value, vendor.Id) - self.assertEquals(query_purchaseorder.APAccountRef.value, account.Id) - self.assertEquals(query_purchaseorder.TotalAmt, 100) + self.assertEqual(query_purchaseorder.VendorRef.value, vendor.Id) + self.assertEqual(query_purchaseorder.APAccountRef.value, account.Id) + self.assertEqual(query_purchaseorder.TotalAmt, 100) query_detail_line = query_purchaseorder.Line[0] - self.assertEquals(query_detail_line.Amount, 100) - self.assertEquals(query_detail_line.ItemBasedExpenseLineDetail.UnitPrice, 100) - self.assertEquals(query_detail_line.ItemBasedExpenseLineDetail.Qty, 1) - self.assertEquals(query_detail_line.ItemBasedExpenseLineDetail.CustomerRef.value, customer.Id) - self.assertEquals(query_detail_line.ItemBasedExpenseLineDetail.TaxCodeRef.value, taxcode.Name) - self.assertEquals(query_detail_line.ItemBasedExpenseLineDetail.ItemRef.value, item.Id) + self.assertEqual(query_detail_line.Amount, 100) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.UnitPrice, 100) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.Qty, 1) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.CustomerRef.value, customer.Id) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.TaxCodeRef.value, taxcode.Name) + self.assertEqual(query_detail_line.ItemBasedExpenseLineDetail.ItemRef.value, item.Id) diff --git a/tests/integration/test_recurringtransaction.py b/tests/integration/test_recurringtransaction.py new file mode 100644 index 00000000..e3588c8e --- /dev/null +++ b/tests/integration/test_recurringtransaction.py @@ -0,0 +1,170 @@ +from datetime import datetime, timedelta +from quickbooks.objects.base import Ref +from quickbooks.objects.customer import Customer +from quickbooks.objects.detailline import SalesItemLine, SalesItemLineDetail, AccountBasedExpenseLine, AccountBasedExpenseLineDetail +from quickbooks.objects.recurringtransaction import RecurringTransaction, RecurringInfo, ScheduleInfo, RecurringInvoice, RecurringBill +from quickbooks.objects.item import Item +from quickbooks.objects.vendor import Vendor +from tests.integration.test_base import QuickbooksTestCase + + +class RecurringTransactionTest(QuickbooksTestCase): + def setUp(self): + super(RecurringTransactionTest, self).setUp() + self.now = datetime.now() + + def create_recurring_invoice(self, t): + # Regular Invoice stuff except use a RecurringInvoice + line = SalesItemLine() + line.LineNum = 1 + line.Description = "description" + line.Amount = 100 + + line.SalesItemLineDetail = SalesItemLineDetail() + item = Item.all(max_results=1, qb=self.qb_client)[0] + line.SalesItemLineDetail.ItemRef = item.to_ref() + + invoice = RecurringInvoice() + + invoice.Line.append(line) + + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + invoice.CustomerRef = customer.to_ref() + + # Now the recurring bits + info = RecurringInfo() + info.Active = True + info.RecurType = "Automated" + info.Name = "Test Recurring Invoice {}".format(t.strftime('%d%H%M%S')) + + info.ScheduleInfo = ScheduleInfo() + info.ScheduleInfo.StartDate = t.strftime("%Y-%m-%d") + info.ScheduleInfo.MaxOccurrences = 6 + info.ScheduleInfo.IntervalType = "Monthly" + info.ScheduleInfo.DayOfMonth = 1 + info.ScheduleInfo.NumInterval = 1 + invoice.RecurringInfo = info + + rt = RecurringTransaction() + rt.Invoice = invoice + + return rt.save(qb=self.qb_client) + + + def test_create_recurring_invoice(self): + actual_rt = self.create_recurring_invoice(self.now) + + self.assertTrue(hasattr(actual_rt, "Invoice")) + self.assertEqual(actual_rt.Invoice.Line[0].Description, "description") + self.assertEqual(actual_rt.Invoice.Line[0].Amount, 100.0) + + actual_info = actual_rt.Invoice.RecurringInfo + self.assertEqual(actual_info.ScheduleInfo.MaxOccurrences, 6) + self.assertEqual(actual_info.ScheduleInfo.IntervalType, "Monthly") + self.assertEqual(actual_info.ScheduleInfo.DayOfMonth, 1) + self.assertEqual(actual_info.ScheduleInfo.NumInterval, 1) + + + def test_create_recurring_bill(self): + bill = RecurringBill() + + line = AccountBasedExpenseLine() + line.Amount = 500 + line.DetailType = "AccountBasedExpenseLineDetail" + + account_ref = Ref() + account_ref.type = "Account" + account_ref.value = 1 + line.AccountBasedExpenseLineDetail = AccountBasedExpenseLineDetail() + line.AccountBasedExpenseLineDetail.AccountRef = account_ref + bill.Line.append(line) + + vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] + bill.VendorRef = vendor.to_ref() + + recurring_info = RecurringInfo() + recurring_info.Active = True + recurring_info.RecurType = "Automated" + recurring_info.Name = "Test Recurring Bill {}".format(datetime.now().strftime('%d%H%M%S')) + + recurring_info.ScheduleInfo = ScheduleInfo() + recurring_info.ScheduleInfo.StartDate = self.now.strftime("%Y-%m-%d") + + end_date = self.now + timedelta(weeks=12) + recurring_info.ScheduleInfo.EndDate = end_date.strftime("%Y-%m-%d") + + recurring_info.ScheduleInfo.NumInterval = 1 + recurring_info.ScheduleInfo.DaysBefore = 3 + recurring_info.ScheduleInfo.IntervalType = "Weekly" + recurring_info.ScheduleInfo.DayOfWeek = "Friday" + + bill.RecurringInfo = recurring_info + + recurring_txn = RecurringTransaction() + recurring_txn.Bill = bill + + saved = recurring_txn.save(qb=self.qb_client) + + actual_rt = RecurringTransaction.get(saved.Bill.Id, qb=self.qb_client) + + self.assertTrue(hasattr(actual_rt, "Bill")) + actual_info = actual_rt.Bill.RecurringInfo + self.assertEqual(actual_info.ScheduleInfo.EndDate, end_date.strftime("%Y-%m-%d")) + self.assertEqual(actual_info.ScheduleInfo.IntervalType, "Weekly") + self.assertEqual(actual_info.ScheduleInfo.DayOfWeek, "Friday") + self.assertEqual(actual_info.ScheduleInfo.NumInterval, 1) + + + def test_update_recurring_invoice(self): + saved = self.create_recurring_invoice(self.now + timedelta(seconds=+1)) # add a second to not conflict with the other test + recurring_txn = RecurringTransaction.get(saved.Invoice.Id, qb=self.qb_client) + + recurring_txn.Invoice.RecurringInfo.ScheduleInfo.DayOfMonth = 15 + recurring_txn.Invoice.Line[0].Amount = 250 + + # QBO api returns this to us as 0 but if you send it back you get an error + recurring_txn.Invoice.Deposit = None + + recurring_txn.save(qb=self.qb_client) + + actual = RecurringTransaction.get(saved.Invoice.Id, qb=self.qb_client) + self.assertEqual(actual.Invoice.RecurringInfo.ScheduleInfo.DayOfMonth, 15) + self.assertEqual(actual.Invoice.Line[0].Amount, 250) + + + def test_filter_by_type(self): + recurring_txns = RecurringTransaction.where("Type = 'Bill'", qb=self.qb_client) + + for recurring_txn in recurring_txns: + self.assertTrue(hasattr(recurring_txn, "Bill")) + + + # this one is mostly to demostrate how to use this + def test_get_all(self): + recurring_txns = RecurringTransaction.all(qb=self.qb_client) + + self.assertGreater(len(recurring_txns), 1) + + types = set() + + for recurring_txn in recurring_txns: + if hasattr(recurring_txn, "Invoice"): + types.add("Invoice") + elif hasattr(recurring_txn, "Bill"): + types.add("Bill") + elif hasattr(recurring_txn, "Purchase"): + types.add("Purchase") + # etc... + + self.assertIn("Bill", types) + self.assertIn("Invoice", types) + + + def test_delete_recurring_invoice(self): + # add a second to not conflict with the other test + saved = self.create_recurring_invoice(self.now + timedelta(seconds=+1)) + recurring_txn = RecurringTransaction.get(saved.Invoice.Id, qb=self.qb_client) + + recurring_txn.delete(qb=self.qb_client) + + diff --git a/tests/integration/test_salesreceipt.py b/tests/integration/test_salesreceipt.py new file mode 100644 index 00000000..ce3bd2a9 --- /dev/null +++ b/tests/integration/test_salesreceipt.py @@ -0,0 +1,59 @@ +from datetime import datetime + +from quickbooks.objects import SalesReceipt, Customer, \ + SalesItemLine, SalesItemLineDetail, Item +from tests.integration.test_base import QuickbooksTestCase + + +class SalesReceiptTest(QuickbooksTestCase): + def setUp(self): + super(SalesReceiptTest, self).setUp() + + self.account_number = datetime.now().strftime('%d%H%M') + self.name = "Test Account {0}".format(self.account_number) + + def create_sales_receipt(self, qty=1, unit_price=100.0): + sales_receipt = SalesReceipt() + sales_receipt.TotalAmt = qty * unit_price + customer = Customer.all(max_results=1, qb=self.qb_client)[0] + sales_receipt.CustomerRef = customer.to_ref() + item = Item.all(max_results=1, qb=self.qb_client)[0] + line = SalesItemLine() + sales_item_line_detail = SalesItemLineDetail() + sales_item_line_detail.ItemRef = item.to_ref() + sales_item_line_detail.Qty = qty + sales_item_line_detail.UnitPrice = unit_price + today = datetime.now() + sales_item_line_detail.ServiceDate = today.strftime( + "%Y-%m-%d" + ) + line.SalesItemLineDetail = sales_item_line_detail + line.Amount = qty * unit_price + sales_receipt.Line = [line] + + return sales_receipt.save(qb=self.qb_client) + + def test_create(self): + sales_receipt = self.create_sales_receipt( + qty=1, + unit_price=100.0 + ) + query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client) + + self.assertEqual(query_sales_receipt.TotalAmt, 100.0) + self.assertEqual(query_sales_receipt.Line[0].Amount, 100.0) + self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['Qty'], 1) + self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['UnitPrice'], 100.0) + + def test_void(self): + sales_receipt = self.create_sales_receipt( + qty=1, + unit_price=100.0 + ) + query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client) + self.assertEqual(query_sales_receipt.TotalAmt, 100.0) + self.assertNotIn('Voided', query_sales_receipt.PrivateNote) + sales_receipt.void(qb=self.qb_client) + query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client) + self.assertEqual(query_sales_receipt.TotalAmt, 0.0) + self.assertIn('Voided', query_sales_receipt.PrivateNote) diff --git a/tests/integration/test_taxagency.py b/tests/integration/test_taxagency.py index 630cc8f6..c99fb0c4 100644 --- a/tests/integration/test_taxagency.py +++ b/tests/integration/test_taxagency.py @@ -8,7 +8,7 @@ class TaxAgencyTest(QuickbooksTestCase): def test_read(self): tax_agencies = TaxAgency.all(max_results=1, qb=self.qb_client) - self.assertEquals(len(tax_agencies), 1) + self.assertEqual(len(tax_agencies), 1) def test_create(self): tax_agency = TaxAgency() @@ -20,5 +20,5 @@ def test_create(self): query_tax_agency = TaxAgency.get(tax_agency.Id, qb=self.qb_client) - self.assertEquals(query_tax_agency.Id, tax_agency.Id) - self.assertEquals(query_tax_agency.DisplayName, name) \ No newline at end of file + self.assertEqual(query_tax_agency.Id, tax_agency.Id) + self.assertEqual(query_tax_agency.DisplayName, name) \ No newline at end of file diff --git a/tests/integration/test_taxcode.py b/tests/integration/test_taxcode.py index c9001e13..6bb1838d 100644 --- a/tests/integration/test_taxcode.py +++ b/tests/integration/test_taxcode.py @@ -9,5 +9,5 @@ def test_get_all(self): # KNOWN Quickbooks bug - TaxCode query returns 3 extra items: # https://intuitdeveloper.lc.intuit.com/questions/1398164-setting-maxresults-on-taxcode-query-returns-incorrect-number-of-records - self.assertEquals(len(tax_codes), 4) + self.assertEqual(len(tax_codes), 4) diff --git a/tests/integration/test_taxrate.py b/tests/integration/test_taxrate.py index 239fdab6..d00599c8 100644 --- a/tests/integration/test_taxrate.py +++ b/tests/integration/test_taxrate.py @@ -6,5 +6,5 @@ class TaxRateTest(QuickbooksTestCase): def test_read(self): tax_rates = TaxRate.all(max_results=1, qb=self.qb_client) - self.assertEquals(len(tax_rates), 1) + self.assertEqual(len(tax_rates), 1) diff --git a/tests/integration/test_taxservice.py b/tests/integration/test_taxservice.py index 07d98baf..73c0c50f 100644 --- a/tests/integration/test_taxservice.py +++ b/tests/integration/test_taxservice.py @@ -24,10 +24,10 @@ def test_create(self): created_taxservice = taxservice.save(qb=self.qb_client) - self.assertEquals(created_taxservice.TaxCode, self.name) + self.assertEqual(created_taxservice.TaxCode, self.name) detail = created_taxservice.TaxRateDetails[0] - self.assertEquals(detail.TaxRateName, self.name) - self.assertEquals(detail.RateValue, 10) - self.assertEquals(detail.TaxAgencyId, '1') - self.assertEquals(detail.TaxApplicableOn, "Sales") + self.assertEqual(detail.TaxRateName, self.name) + self.assertEqual(detail.RateValue, 10) + self.assertEqual(detail.TaxAgencyId, '1') + self.assertEqual(detail.TaxApplicableOn, "Sales") diff --git a/tests/integration/test_term.py b/tests/integration/test_term.py index e3c7fd63..9c3ff510 100644 --- a/tests/integration/test_term.py +++ b/tests/integration/test_term.py @@ -18,9 +18,9 @@ def test_create(self): query_term = Term.get(term.Id, qb=self.qb_client) - self.assertEquals(query_term.Id, term.Id) - self.assertEquals(query_term.Name, self.name) - self.assertEquals(query_term.DueDays, 10) + self.assertEqual(query_term.Id, term.Id) + self.assertEqual(query_term.Name, self.name) + self.assertEqual(query_term.DueDays, 10) def test_update(self): term = Term.all(max_results=1, qb=self.qb_client)[0] @@ -29,5 +29,5 @@ def test_update(self): query_term = Term.get(term.Id, qb=self.qb_client) - self.assertEquals(query_term.Id, term.Id) - self.assertEquals(query_term.DueDays, 60) + self.assertEqual(query_term.Id, term.Id) + self.assertEqual(query_term.DueDays, 60) diff --git a/tests/integration/test_timeactivity.py b/tests/integration/test_timeactivity.py index 5f908810..39ad1a0b 100644 --- a/tests/integration/test_timeactivity.py +++ b/tests/integration/test_timeactivity.py @@ -22,24 +22,28 @@ 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) - self.assertEquals(query_time_activity.Id, time_activity.Id) - self.assertEquals(query_time_activity.NameOf, "Employee") - self.assertEquals(query_time_activity.Description, "Test description") - self.assertEquals(query_time_activity.EmployeeRef.value, employee.Id) + self.assertEqual(query_time_activity.Id, time_activity.Id) + 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.assertEquals(query_time_activity.StartTime, '2016-07-22T10:00:00-07:00') - #self.assertEquals(query_time_activity.EndTime, '2016-07-22T11:00:00-07:00') + #self.assertEqual(query_time_activity.StartTime, '2016-07-22T10:00:00-07:00') + #self.assertEqual(query_time_activity.EndTime, '2016-07-22T11:00:00-07:00') 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.assertEquals(query_time_activity.Description, "Updated test description") + self.assertEqual(query_time_activity.Description, "Updated test description") + self.assertEqual(query_time_activity.CostRate, 75.0) diff --git a/tests/integration/test_trackingclass.py b/tests/integration/test_trackingclass.py index 8cd40473..ec7d796a 100644 --- a/tests/integration/test_trackingclass.py +++ b/tests/integration/test_trackingclass.py @@ -17,8 +17,8 @@ def test_create(self): query_tracking_class = Class.get(tracking_class.Id, qb=self.qb_client) - self.assertEquals(query_tracking_class.Id, tracking_class.Id) - self.assertEquals(query_tracking_class.Name, self.name) + self.assertEqual(query_tracking_class.Id, tracking_class.Id) + self.assertEqual(query_tracking_class.Name, self.name) def test_update(self): updated_name = "Updated {}".format(self.name) @@ -29,5 +29,5 @@ def test_update(self): query_tracking_class = Class.get(tracking_class.Id, qb=self.qb_client) - self.assertEquals(query_tracking_class.Id, tracking_class.Id) - self.assertEquals(query_tracking_class.Name, updated_name) + self.assertEqual(query_tracking_class.Id, tracking_class.Id) + self.assertEqual(query_tracking_class.Name, updated_name) diff --git a/tests/integration/test_transfer.py b/tests/integration/test_transfer.py index 86ec58f1..8eddb31f 100644 --- a/tests/integration/test_transfer.py +++ b/tests/integration/test_transfer.py @@ -31,10 +31,10 @@ def test_create(self): query_transfer = Transfer.get(transfer.Id, qb=self.qb_client) - self.assertEquals(query_transfer.Id, transfer.Id) - self.assertEquals(query_transfer.Amount, 100) - self.assertEquals(query_transfer.FromAccountRef.value, from_account.Id) - self.assertEquals(query_transfer.ToAccountRef.value, to_account.Id) + self.assertEqual(query_transfer.Id, transfer.Id) + self.assertEqual(query_transfer.Amount, 100) + self.assertEqual(query_transfer.FromAccountRef.value, from_account.Id) + self.assertEqual(query_transfer.ToAccountRef.value, to_account.Id) # reset transfer (so the from_account doesn't run out of cash) transfer = Transfer() @@ -51,4 +51,4 @@ def test_update(self): query_transfer = Transfer.get(transfer.Id, qb=self.qb_client) - self.assertEquals(query_transfer.Amount, transfer.Amount) + self.assertEqual(query_transfer.Amount, transfer.Amount) diff --git a/tests/integration/test_vendor.py b/tests/integration/test_vendor.py index 679e1f2f..a1b8d631 100644 --- a/tests/integration/test_vendor.py +++ b/tests/integration/test_vendor.py @@ -46,26 +46,26 @@ def test_create(self): query_vendor = Vendor.get(vendor.Id, qb=self.qb_client) - self.assertEquals(query_vendor.Id, vendor.Id) - - self.assertEquals(query_vendor.AcctNum, self.account_number) - self.assertEquals(query_vendor.Title, 'Ms.') - self.assertEquals(query_vendor.GivenName, 'First') - self.assertEquals(query_vendor.FamilyName, 'Last') - self.assertEquals(query_vendor.Suffix, 'Sr.') - self.assertEquals(query_vendor.CompanyName, self.name) - self.assertEquals(query_vendor.DisplayName, self.name) - self.assertEquals(query_vendor.PrintOnCheckName, self.name) - - self.assertEquals(query_vendor.BillAddr.Line1, "123 Main") - self.assertEquals(query_vendor.BillAddr.Line2, "Apartment 1") - self.assertEquals(query_vendor.BillAddr.City, "City") - self.assertEquals(query_vendor.BillAddr.Country, "U.S.A") - self.assertEquals(query_vendor.BillAddr.CountrySubDivisionCode, "CA") - self.assertEquals(query_vendor.BillAddr.PostalCode, "94030") - self.assertEquals(query_vendor.PrimaryPhone.FreeFormNumber, '555-555-5555') - self.assertEquals(query_vendor.PrimaryEmailAddr.Address, 'test@email.com') - self.assertEquals(query_vendor.WebAddr.URI, 'http://testurl.com/') + self.assertEqual(query_vendor.Id, vendor.Id) + + self.assertEqual(query_vendor.AcctNum, self.account_number) + self.assertEqual(query_vendor.Title, 'Ms.') + self.assertEqual(query_vendor.GivenName, 'First') + self.assertEqual(query_vendor.FamilyName, 'Last') + self.assertEqual(query_vendor.Suffix, 'Sr.') + self.assertEqual(query_vendor.CompanyName, self.name) + self.assertEqual(query_vendor.DisplayName, self.name) + self.assertEqual(query_vendor.PrintOnCheckName, self.name) + + self.assertEqual(query_vendor.BillAddr.Line1, "123 Main") + self.assertEqual(query_vendor.BillAddr.Line2, "Apartment 1") + self.assertEqual(query_vendor.BillAddr.City, "City") + self.assertEqual(query_vendor.BillAddr.Country, "U.S.A") + self.assertEqual(query_vendor.BillAddr.CountrySubDivisionCode, "CA") + self.assertEqual(query_vendor.BillAddr.PostalCode, "94030") + self.assertEqual(query_vendor.PrimaryPhone.FreeFormNumber, '555-555-5555') + self.assertEqual(query_vendor.PrimaryEmailAddr.Address, 'test@email.com') + self.assertEqual(query_vendor.WebAddr.URI, 'http://testurl.com/') def update_vendor(self): vendor = Vendor.all(max_results=1, qb=self.qb_client)[0] @@ -76,5 +76,5 @@ def update_vendor(self): vendor.save(qb=self.qb_client) query_vendor = Vendor.get(vendor.Id, qb=self.qb_client) - self.assertEquals(query_vendor.GivenName, 'Updated Name') - self.assertEquals(query_vendor.FamilyName, 'Updated Lastname') + self.assertEqual(query_vendor.GivenName, 'Updated Name') + self.assertEqual(query_vendor.FamilyName, 'Updated Lastname') diff --git a/tests/unit/objects/test_account.py b/tests/unit/objects/test_account.py index b9d38c47..6da0a1bd 100644 --- a/tests/unit/objects/test_account.py +++ b/tests/unit/objects/test_account.py @@ -9,7 +9,7 @@ def test_unicode(self): account = Account() account.FullyQualifiedName = "test" - self.assertEquals(str(account), "test") + self.assertEqual(str(account), "test") def test_to_ref(self): account = Account() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = account.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Account") - self.assertEquals(ref.value, 12) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Account") + self.assertEqual(ref.value, 12) def test_valid_object_name(self): account = Account() diff --git a/tests/unit/objects/test_attachable.py b/tests/unit/objects/test_attachable.py index e50e7221..26080f62 100644 --- a/tests/unit/objects/test_attachable.py +++ b/tests/unit/objects/test_attachable.py @@ -9,7 +9,7 @@ def test_unicode(self): attachable = Attachable() attachable.FileName = "test" - self.assertEquals(str(attachable), "test") + self.assertEqual(str(attachable), "test") def test_to_ref(self): attachable = Attachable() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = attachable.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Attachable") - self.assertEquals(ref.value, 12) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Attachable") + self.assertEqual(ref.value, 12) def test_valid_object_name(self): attachable = Attachable() diff --git a/tests/unit/objects/test_base.py b/tests/unit/objects/test_base.py index 128aa086..d13e8c76 100644 --- a/tests/unit/objects/test_base.py +++ b/tests/unit/objects/test_base.py @@ -14,7 +14,7 @@ def test_unicode(self): address.CountrySubDivisionCode = "MO" address.PostalCode = "12345" - self.assertEquals(str(address), "123 Main Joplin, MO 12345") + self.assertEqual(str(address), "123 Main Joplin, MO 12345") class PhoneNumberTests(unittest.TestCase): @@ -22,7 +22,7 @@ def test_unicode(self): number = PhoneNumber() number.FreeFormNumber = "555-555-5555" - self.assertEquals(str(number), "555-555-5555") + self.assertEqual(str(number), "555-555-5555") class EmailAddressTests(unittest.TestCase): @@ -30,7 +30,7 @@ def test_unicode(self): email = EmailAddress() email.Address = "email@gmail.com" - self.assertEquals(str(email), "email@gmail.com") + self.assertEqual(str(email), "email@gmail.com") class WebAddressTests(unittest.TestCase): @@ -38,7 +38,7 @@ def test_unicode(self): url = WebAddress() url.URI = "www.website.com" - self.assertEquals(str(url), "www.website.com") + self.assertEqual(str(url), "www.website.com") class RefTests(unittest.TestCase): @@ -48,7 +48,7 @@ def test_unicode(self): ref.name = "test" ref.value = 1 - self.assertEquals(str(ref), "test") + self.assertEqual(str(ref), "test") class CustomFieldTests(unittest.TestCase): @@ -56,7 +56,7 @@ def test_unicode(self): custom = CustomField() custom.Name = "name" - self.assertEquals(str(custom), "name") + self.assertEqual(str(custom), "name") class CustomerMemoTests(unittest.TestCase): @@ -64,7 +64,7 @@ def test_unicode(self): memo = CustomerMemo() memo.value = "value" - self.assertEquals(str(memo), "value") + self.assertEqual(str(memo), "value") class LinkedTxnTests(unittest.TestCase): @@ -72,7 +72,7 @@ def test_unicode(self): linked = LinkedTxn() linked.TxnId = 1 - self.assertEquals(str(linked), "1") + self.assertEqual(str(linked), "1") class MetaDataTests(unittest.TestCase): @@ -80,17 +80,17 @@ def test_unicode(self): meta = MetaData() meta.CreateTime = "1/1/2000" - self.assertEquals(str(meta), "Created 1/1/2000") + self.assertEqual(str(meta), "Created 1/1/2000") class MarkupInfoTests(unittest.TestCase): def test_init(self): markup = MarkupInfo() - self.assertEquals(markup.PercentBased, False) - self.assertEquals(markup.Value, 0) - self.assertEquals(markup.Percent, 0) - self.assertEquals(markup.PriceLevelRef, None) + self.assertEqual(markup.PercentBased, False) + self.assertEqual(markup.Value, 0) + self.assertEqual(markup.Percent, 0) + self.assertEqual(markup.PriceLevelRef, None) class AttachableRefTests(unittest.TestCase): @@ -101,11 +101,11 @@ def test_init(self): attachable.Inactive = False attachable.NoRefOnly = False - self.assertEquals(attachable.LineInfo, None) - self.assertEquals(attachable.IncludeOnSend, False) - self.assertEquals(attachable.Inactive, False) - self.assertEquals(attachable.NoRefOnly, False) - self.assertEquals(attachable.EntityRef, None) + self.assertEqual(attachable.LineInfo, None) + self.assertEqual(attachable.IncludeOnSend, False) + self.assertEqual(attachable.Inactive, False) + self.assertEqual(attachable.NoRefOnly, False) + self.assertEqual(attachable.EntityRef, None) class LinkedTxnMixinTests(unittest.TestCase): @@ -116,6 +116,6 @@ def test_to_linked_txn(self): linked_txn = deposit.to_linked_txn() - self.assertEquals(linked_txn.TxnId, 100) - self.assertEquals(linked_txn.TxnType, "Deposit") - self.assertEquals(linked_txn.TxnLineId, 1) + self.assertEqual(linked_txn.TxnId, 100) + self.assertEqual(linked_txn.TxnType, "Deposit") + self.assertEqual(linked_txn.TxnLineId, 1) diff --git a/tests/unit/objects/test_batchrequest.py b/tests/unit/objects/test_batchrequest.py index 8a83727f..cb68ef50 100644 --- a/tests/unit/objects/test_batchrequest.py +++ b/tests/unit/objects/test_batchrequest.py @@ -10,7 +10,7 @@ def test__repr__(self): fault.original_object = 100 fault.Error.append("error") - self.assertEquals(str(fault.__repr__()), "1 Errors") + self.assertEqual(str(fault.__repr__()), "1 Errors") class FaultErrorTests(unittest.TestCase): @@ -20,7 +20,7 @@ def test_unicode(self): fault_error.code = 100 fault_error.Detail = "detail" - self.assertEquals(str(fault_error), "Code: 100 Message: test Detail: detail") + self.assertEqual(str(fault_error), "Code: 100 Message: test Detail: detail") def test__repr__(self): fault_error = FaultError() @@ -28,7 +28,7 @@ def test__repr__(self): fault_error.code = 100 fault_error.Detail = "detail" - self.assertEquals(fault_error.__repr__(), "Code: 100 Message: test Detail: detail") + self.assertEqual(fault_error.__repr__(), "Code: 100 Message: test Detail: detail") class BatchItemResponseTests(unittest.TestCase): @@ -37,15 +37,15 @@ def test_set_object(self): batch_item = BatchItemResponse() batch_item.set_object(obj) - self.assertEquals(batch_item._original_object, obj) - self.assertEquals(batch_item.Error, obj) + self.assertEqual(batch_item._original_object, obj) + self.assertEqual(batch_item.Error, obj) def test_get_object(self): obj = Fault() batch_item = BatchItemResponse() batch_item.set_object(obj) - self.assertEquals(batch_item.get_object(), obj) + self.assertEqual(batch_item.get_object(), obj) class BatchItemRequestTests(unittest.TestCase): @@ -54,12 +54,12 @@ def test_set_object(self): batch_item = BatchItemRequest() batch_item.set_object(obj) - self.assertEquals(batch_item._original_object, obj) - self.assertEquals(batch_item.Error, obj) + self.assertEqual(batch_item._original_object, obj) + self.assertEqual(batch_item.Error, obj) def test_get_object(self): obj = Fault() batch_item = BatchItemRequest() batch_item.set_object(obj) - self.assertEquals(batch_item.get_object(), obj) + self.assertEqual(batch_item.get_object(), obj) diff --git a/tests/unit/objects/test_bill.py b/tests/unit/objects/test_bill.py index 383dca05..91c983f2 100644 --- a/tests/unit/objects/test_bill.py +++ b/tests/unit/objects/test_bill.py @@ -9,7 +9,7 @@ def test_unicode(self): bill = Bill() bill.Balance = 1000 - self.assertEquals(str(bill), "1000") + self.assertEqual(str(bill), "1000") def test_to_LinkedTxn(self): bill = Bill() @@ -17,9 +17,9 @@ def test_to_LinkedTxn(self): linked_txn = bill.to_linked_txn() - self.assertEquals(linked_txn.TxnId, bill.Id) - self.assertEquals(linked_txn.TxnType, "Bill") - self.assertEquals(linked_txn.TxnLineId, 1) + self.assertEqual(linked_txn.TxnId, bill.Id) + self.assertEqual(linked_txn.TxnType, "Bill") + self.assertEqual(linked_txn.TxnLineId, 1) def test_valid_object_name(self): obj = Bill() @@ -35,7 +35,7 @@ def test_to_ref(self): ref = bill.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Bill") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Bill") + self.assertEqual(ref.value, 100) diff --git a/tests/unit/objects/test_billpayment.py b/tests/unit/objects/test_billpayment.py index b71b3b81..994f6294 100644 --- a/tests/unit/objects/test_billpayment.py +++ b/tests/unit/objects/test_billpayment.py @@ -9,7 +9,7 @@ def test_unicode(self): checkpayment = CheckPayment() checkpayment.PrintStatus = "test" - self.assertEquals(str(checkpayment), "test") + self.assertEqual(str(checkpayment), "test") class BillPaymentLineTests(unittest.TestCase): @@ -17,7 +17,7 @@ def test_unicode(self): bill = BillPaymentLine() bill.Amount = 1000 - self.assertEquals(str(bill), "1000") + self.assertEqual(str(bill), "1000") class BillPaymentTests(unittest.TestCase): @@ -25,7 +25,7 @@ def test_unicode(self): bill_payment = BillPayment() bill_payment.TotalAmt = 1000 - self.assertEquals(str(bill_payment), "1000") + self.assertEqual(str(bill_payment), "1000") def test_valid_object_name(self): obj = BillPayment() @@ -39,4 +39,4 @@ class BillPaymentCreditCardTests(unittest.TestCase): def test_init(self): bill_payment_cc = BillPaymentCreditCard() - self.assertEquals(bill_payment_cc.CCAccountRef, None) + self.assertEqual(bill_payment_cc.CCAccountRef, None) diff --git a/tests/unit/objects/test_budget.py b/tests/unit/objects/test_budget.py index fc363d2c..7fc5aee5 100644 --- a/tests/unit/objects/test_budget.py +++ b/tests/unit/objects/test_budget.py @@ -8,7 +8,7 @@ def test_unicode(self): budget_detail = BudgetDetail() budget_detail.Amount = 10 - self.assertEquals(str(budget_detail), "10") + self.assertEqual(str(budget_detail), "10") class BudgetTests(unittest.TestCase): @@ -16,4 +16,4 @@ def test_unicode(self): budget = Budget() budget.Name = "test" - self.assertEquals(str(budget), "test") + self.assertEqual(str(budget), "test") diff --git a/tests/unit/objects/test_company_info.py b/tests/unit/objects/test_company_info.py index e8f02b79..bb99992c 100644 --- a/tests/unit/objects/test_company_info.py +++ b/tests/unit/objects/test_company_info.py @@ -9,7 +9,7 @@ def test_unicode(self): company_info = CompanyInfo() company_info.CompanyName = "test" - self.assertEquals(str(company_info), "test") + self.assertEqual(str(company_info), "test") def test_to_ref(self): company_info = CompanyInfo() @@ -18,6 +18,6 @@ def test_to_ref(self): ref = company_info.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "CompanyInfo") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "CompanyInfo") + self.assertEqual(ref.value, 100) diff --git a/tests/unit/objects/test_companycurrency.py b/tests/unit/objects/test_companycurrency.py index fec2f401..34a1a10f 100644 --- a/tests/unit/objects/test_companycurrency.py +++ b/tests/unit/objects/test_companycurrency.py @@ -1,24 +1,24 @@ -from datetime import datetime - +import unittest from quickbooks.objects.companycurrency import CompanyCurrency -from tests.integration.test_base import QuickbooksUnitTestCase -class CompanyCurrencyTest(QuickbooksUnitTestCase): +class CompanyCurrencyTest(unittest.TestCase): def test_unicode(self): company_currency = CompanyCurrency() company_currency.Name = "test" company_currency.Code = "USD" - self.assertEquals(str(company_currency), "test") + self.assertEqual(str(company_currency), "test") def test_to_ref(self): company_currency = CompanyCurrency() company_currency.Name = "test" + company_currency.Code = "USD" company_currency.Id = 23 ref = company_currency.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "CompanyCurrency") - self.assertEquals(ref.value, 23) \ No newline at end of file + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "CompanyCurrency") + self.assertEqual(ref.value, "USD") + diff --git a/tests/unit/objects/test_creditcardpayment.py b/tests/unit/objects/test_creditcardpayment.py index 723c624d..5591a616 100644 --- a/tests/unit/objects/test_creditcardpayment.py +++ b/tests/unit/objects/test_creditcardpayment.py @@ -7,29 +7,29 @@ class CreditCardPaymentTests(unittest.TestCase): def test_init(self): payment = CreditCardPayment() - self.assertEquals(payment.CreditChargeInfo, None) - self.assertEquals(payment.CreditChargeResponse, None) + self.assertEqual(payment.CreditChargeInfo, None) + self.assertEqual(payment.CreditChargeResponse, None) class CreditChargeResponseTests(unittest.TestCase): def test_init(self): response = CreditChargeResponse() - self.assertEquals(response.CCTransId, "") - self.assertEquals(response.AuthCode, "") - self.assertEquals(response.TxnAuthorizationTime, "") - self.assertEquals(response.Status, "") + self.assertEqual(response.CCTransId, "") + self.assertEqual(response.AuthCode, "") + self.assertEqual(response.TxnAuthorizationTime, "") + self.assertEqual(response.Status, "") class CreditChargeInfoTests(unittest.TestCase): def test_init(self): info = CreditChargeInfo() - self.assertEquals(info.Type, "") - self.assertEquals(info.NameOnAcct, "") - self.assertEquals(info.CcExpiryMonth, 0) - self.assertEquals(info.CcExpiryYear, 0) - self.assertEquals(info.BillAddrStreet, "") - self.assertEquals(info.PostalCode, "") - self.assertEquals(info.Amount, 0) - self.assertEquals(info.ProcessPayment, False) + self.assertEqual(info.Type, "") + self.assertEqual(info.NameOnAcct, "") + self.assertEqual(info.CcExpiryMonth, 0) + self.assertEqual(info.CcExpiryYear, 0) + self.assertEqual(info.BillAddrStreet, "") + self.assertEqual(info.PostalCode, "") + self.assertEqual(info.Amount, 0) + self.assertEqual(info.ProcessPayment, False) diff --git a/tests/unit/objects/test_creditcardpayment_entity.py b/tests/unit/objects/test_creditcardpayment_entity.py index 2a99a255..1e7be666 100644 --- a/tests/unit/objects/test_creditcardpayment_entity.py +++ b/tests/unit/objects/test_creditcardpayment_entity.py @@ -9,7 +9,7 @@ def test_unicode(self): credit_card_payment = CreditCardPayment() credit_card_payment.Amount = 100 - self.assertEquals(str(credit_card_payment), "100") + self.assertEqual(str(credit_card_payment), "100") def test_valid_object_name(self): obj = CreditCardPayment() diff --git a/tests/unit/objects/test_creditmemo.py b/tests/unit/objects/test_creditmemo.py index 932f71c7..0935fecc 100644 --- a/tests/unit/objects/test_creditmemo.py +++ b/tests/unit/objects/test_creditmemo.py @@ -11,7 +11,7 @@ def test_unicode(self): detail = SalesItemLineDetail() detail.UnitPrice = 10 - self.assertEquals(str(detail), "10") + self.assertEqual(str(detail), "10") class CreditMemoTests(unittest.TestCase): @@ -19,7 +19,7 @@ def test_unicode(self): credit_memo = CreditMemo() credit_memo.TotalAmt = 1000 - self.assertEquals(str(credit_memo), "1000") + self.assertEqual(str(credit_memo), "1000") def test_valid_object_name(self): obj = CreditMemo() @@ -28,36 +28,44 @@ def test_valid_object_name(self): self.assertTrue(result) + def test_to_ref(self): + obj = CreditMemo() + obj.Id = 123 + + ref = obj.to_ref() + self.assertEqual(ref.value, obj.Id) + self.assertEqual(ref.type, "CreditMemo") + class DiscountLineDetailTests(unittest.TestCase): def test_init(self): discount_detail = DiscountLineDetail() - self.assertEquals(discount_detail.ClassRef, None) - self.assertEquals(discount_detail.TaxCodeRef, None) - self.assertEquals(discount_detail.Discount, None) + self.assertEqual(discount_detail.ClassRef, None) + self.assertEqual(discount_detail.TaxCodeRef, None) + self.assertEqual(discount_detail.Discount, None) class SubtotalLineDetailTests(unittest.TestCase): def test_init(self): detail = SubtotalLineDetail() - self.assertEquals(detail.ItemRef, None) + self.assertEqual(detail.ItemRef, None) class DiscountOverrideTests(unittest.TestCase): def test_init(self): discount_detail = DiscountOverride() - self.assertEquals(discount_detail.PercentBased, False) - self.assertEquals(discount_detail.DiscountPercent, 0) - self.assertEquals(discount_detail.DiscountAccountRef, None) - self.assertEquals(discount_detail.DiscountRef, None) + self.assertEqual(discount_detail.PercentBased, False) + self.assertEqual(discount_detail.DiscountPercent, 0) + self.assertEqual(discount_detail.DiscountAccountRef, None) + self.assertEqual(discount_detail.DiscountRef, None) class DescriptionLineDetailTests(unittest.TestCase): def test_init(self): detail = DescriptionLineDetail() - self.assertEquals(detail.ServiceDate, "") - self.assertEquals(detail.TaxCodeRef, None) + self.assertEqual(detail.ServiceDate, "") + self.assertEqual(detail.TaxCodeRef, None) diff --git a/tests/unit/objects/test_customer.py b/tests/unit/objects/test_customer.py index 4902b863..e0780f56 100644 --- a/tests/unit/objects/test_customer.py +++ b/tests/unit/objects/test_customer.py @@ -9,7 +9,7 @@ def test_unicode(self): customer = Customer() customer.DisplayName = "test" - self.assertEquals(str(customer), "test") + self.assertEqual(str(customer), "test") def test_to_ref(self): customer = Customer() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = customer.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Customer") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Customer") + self.assertEqual(ref.value, 100) def test_valid_object_name(self): obj = Customer() diff --git a/tests/unit/objects/test_customertype.py b/tests/unit/objects/test_customertype.py new file mode 100644 index 00000000..c459d22d --- /dev/null +++ b/tests/unit/objects/test_customertype.py @@ -0,0 +1,19 @@ +import unittest + +from quickbooks import QuickBooks +from quickbooks.objects.customertype import CustomerType + + +class CustomerTypeTests(unittest.TestCase): + def test_unicode(self): + customer_type = CustomerType() + customer_type.Name = "test" + + self.assertEqual(str(customer_type), "test") + + def test_valid_object_name(self): + obj = CustomerType() + client = QuickBooks() + result = client.isvalid_object_name(obj.qbo_object_name) + + self.assertTrue(result) diff --git a/tests/unit/objects/test_department.py b/tests/unit/objects/test_department.py index 152cc70d..90d130f6 100644 --- a/tests/unit/objects/test_department.py +++ b/tests/unit/objects/test_department.py @@ -9,7 +9,7 @@ def test_unicode(self): department = Department() department.Name = "test" - self.assertEquals(str(department), "test") + self.assertEqual(str(department), "test") def test_to_ref(self): department = Department() @@ -18,9 +18,9 @@ def test_to_ref(self): dept_ref = department.to_ref() - self.assertEquals(dept_ref.name, "test") - self.assertEquals(dept_ref.type, "Department") - self.assertEquals(dept_ref.value, 100) + self.assertEqual(dept_ref.name, "test") + self.assertEqual(dept_ref.type, "Department") + self.assertEqual(dept_ref.value, 100) def test_valid_object_name(self): obj = Department() diff --git a/tests/unit/objects/test_deposit.py b/tests/unit/objects/test_deposit.py index ac74273d..65d7a5bc 100644 --- a/tests/unit/objects/test_deposit.py +++ b/tests/unit/objects/test_deposit.py @@ -9,7 +9,7 @@ def test_unicode(self): deposit = Deposit() deposit.TotalAmt = 100 - self.assertEquals(str(deposit), "100") + self.assertEqual(str(deposit), "100") def test_valid_object_name(self): obj = Deposit() @@ -24,25 +24,25 @@ def test_unicode(self): deposit = DepositLine() deposit.Amount = 100 - self.assertEquals(str(deposit), "100") + self.assertEqual(str(deposit), "100") class CashBackInfoTests(unittest.TestCase): def test_init(self): cash_back_info = CashBackInfo() - self.assertEquals(cash_back_info.Amount, 0) - self.assertEquals(cash_back_info.Memo, "") - self.assertEquals(cash_back_info.AccountRef, None) + self.assertEqual(cash_back_info.Amount, 0) + self.assertEqual(cash_back_info.Memo, "") + self.assertEqual(cash_back_info.AccountRef, None) class DepositLineDetailTests(unittest.TestCase): def test_init(self): detail = DepositLineDetail() - self.assertEquals(detail.Entity, None) - self.assertEquals(detail.ClassRef, None) - self.assertEquals(detail.AccountRef, None) - self.assertEquals(detail.PaymentMethodRef, None) - self.assertEquals(detail.CheckNum, "") - self.assertEquals(detail.TxnType, None) + self.assertEqual(detail.Entity, None) + self.assertEqual(detail.ClassRef, None) + self.assertEqual(detail.AccountRef, None) + self.assertEqual(detail.PaymentMethodRef, None) + self.assertEqual(detail.CheckNum, "") + self.assertEqual(detail.TxnType, None) diff --git a/tests/unit/objects/test_detailline.py b/tests/unit/objects/test_detailline.py index 0ef0ed49..a0bf19df 100644 --- a/tests/unit/objects/test_detailline.py +++ b/tests/unit/objects/test_detailline.py @@ -1,7 +1,7 @@ import unittest from quickbooks.objects.detailline import SalesItemLineDetail, DiscountOverride, DetailLine, SubtotalLineDetail, \ - DiscountLineDetail, SubtotalLine, DescriptionLineDetail, DescriptionLine, SalesItemLine, DiscountLine, GroupLine, \ + DiscountLineDetail, SubtotalLine, DescriptionLineDetail, SalesItemLine, DiscountLine, GroupLine, \ AccountBasedExpenseLineDetail, ItemBasedExpenseLineDetail, DescriptionOnlyLine, ItemBasedExpenseLine @@ -12,7 +12,7 @@ def test_unicode(self): detail.Description = "Product Description" detail.Amount = 100 - self.assertEquals(str(detail), "[1] Product Description 100") + self.assertEqual(str(detail), "[1] Product Description 100") class SalesItemLineDetailTests(unittest.TestCase): @@ -20,16 +20,16 @@ def test_unicode(self): sales_detail = SalesItemLineDetail() sales_detail.UnitPrice = 10 - self.assertEquals(str(sales_detail), "10") + self.assertEqual(str(sales_detail), "10") class DiscountOverrideTests(unittest.TestCase): def test_init(self): discount_override = DiscountOverride() - self.assertEquals(discount_override.DiscountPercent, 0) - self.assertEquals(discount_override.DiscountRef, None) - self.assertEquals(discount_override.DiscountAccountRef, None) + self.assertEqual(discount_override.DiscountPercent, 0) + self.assertEqual(discount_override.DiscountRef, None) + self.assertEqual(discount_override.DiscountAccountRef, None) self.assertFalse(discount_override.PercentBased) @@ -37,87 +37,78 @@ class DiscountLineDetailTesets(unittest.TestCase): def test_init(self): discount_detail = DiscountLineDetail() - self.assertEquals(discount_detail.Discount, None) - self.assertEquals(discount_detail.ClassRef, None) - self.assertEquals(discount_detail.TaxCodeRef, None) + self.assertEqual(discount_detail.Discount, None) + self.assertEqual(discount_detail.ClassRef, None) + self.assertEqual(discount_detail.TaxCodeRef, None) class SubtotalLineDetailTest(unittest.TestCase): def test_init(self): detail = SubtotalLineDetail() - self.assertEquals(detail.ItemRef, None) + self.assertEqual(detail.ItemRef, None) class SubtotalLineTest(unittest.TestCase): def test_init(self): subtotal_line = SubtotalLine() - self.assertEquals(subtotal_line.DetailType, "SubTotalLineDetail") - self.assertEquals(subtotal_line.SubtotalLineDetail, None) + self.assertEqual(subtotal_line.DetailType, "SubTotalLineDetail") + self.assertEqual(subtotal_line.SubtotalLineDetail, None) class DescriptionLineDetailTest(unittest.TestCase): def test_init(self): description_detail = DescriptionLineDetail() - self.assertEquals(description_detail.ServiceDate, "") - self.assertEquals(description_detail.TaxCodeRef, None) - - -class DescriptionLineTest(unittest.TestCase): - def test_init(self): - line = DescriptionLine() - - self.assertEquals(line.DetailType, "DescriptionOnly") - self.assertEquals(line.DescriptionLineDetail, None) + self.assertEqual(description_detail.ServiceDate, "") + self.assertEqual(description_detail.TaxCodeRef, None) class SalesItemLineTest(unittest.TestCase): def test_init(self): line = SalesItemLine() - self.assertEquals(line.DetailType, "SalesItemLineDetail") - self.assertEquals(line.SalesItemLineDetail, None) + self.assertEqual(line.DetailType, "SalesItemLineDetail") + self.assertEqual(line.SalesItemLineDetail, None) class DiscountLineTest(unittest.TestCase): def test_init(self): line = DiscountLine() - self.assertEquals(line.DetailType, "DiscountLineDetail") - self.assertEquals(line.DiscountLineDetail, None) + self.assertEqual(line.DetailType, "DiscountLineDetail") + self.assertEqual(line.DiscountLineDetail, None) class GroupLineTest(unittest.TestCase): def test_init(self): line = GroupLine() - self.assertEquals(line.DetailType, "GroupLineDetail") - self.assertEquals(line.GroupLineDetail, None) + self.assertEqual(line.DetailType, "GroupLineDetail") + self.assertEqual(line.GroupLineDetail, None) class ItemBasedExpenseLineDetailTest(unittest.TestCase): def test_init(self): detail = ItemBasedExpenseLineDetail() - self.assertEquals(detail.BillableStatus, None) - self.assertEquals(detail.UnitPrice, 0) - self.assertEquals(detail.TaxInclusiveAmt, 0) - self.assertEquals(detail.Qty, 0) - self.assertEquals(detail.ItemRef, None) - self.assertEquals(detail.ClassRef, None) - self.assertEquals(detail.PriceLevelRef, None) - self.assertEquals(detail.TaxCodeRef, None) - self.assertEquals(detail.MarkupInfo, None) - self.assertEquals(detail.CustomerRef, None) + self.assertEqual(detail.BillableStatus, None) + self.assertEqual(detail.UnitPrice, 0) + self.assertEqual(detail.Qty, 0) + self.assertEqual(detail.ItemRef, None) + self.assertEqual(detail.ClassRef, None) + self.assertEqual(detail.PriceLevelRef, None) + self.assertEqual(detail.TaxCodeRef, None) + self.assertEqual(detail.MarkupInfo, None) + self.assertEqual(detail.CustomerRef, None) class ItemBasedExpenseLineTests(unittest.TestCase): def test_unicode(self): line = ItemBasedExpenseLine() - self.assertEquals(line.DetailType, "ItemBasedExpenseLineDetail") - self.assertEquals(line.ItemBasedExpenseLineDetail, None) + self.assertEqual(line.DetailType, "ItemBasedExpenseLineDetail") + self.assertEqual(line.ItemBasedExpenseLineDetail, None) class AccountBasedExpenseLineDetailTests(unittest.TestCase): @@ -125,12 +116,12 @@ def test_unicode(self): acct_detail = AccountBasedExpenseLineDetail() acct_detail.BillableStatus = "test" - self.assertEquals(str(acct_detail), "test") + self.assertEqual(str(acct_detail), "test") class DescriptionOnlyLineTests(unittest.TestCase): def test_unicode(self): line = DescriptionOnlyLine() - self.assertEquals(line.DetailType, "DescriptionOnly") - self.assertEquals(line.DescriptionLineDetail, None) + self.assertEqual(line.DetailType, "DescriptionOnly") + self.assertEqual(line.DescriptionLineDetail, None) diff --git a/tests/unit/objects/test_employee.py b/tests/unit/objects/test_employee.py index 9b1db4d3..cf2f540f 100644 --- a/tests/unit/objects/test_employee.py +++ b/tests/unit/objects/test_employee.py @@ -9,7 +9,7 @@ def test_unicode(self): employee = Employee() employee.DisplayName = "test" - self.assertEquals(str(employee), "test") + self.assertEqual(str(employee), "test") def test_to_ref(self): employee = Employee() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = employee.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Employee") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Employee") + self.assertEqual(ref.value, 100) def test_valid_object_name(self): obj = Employee() diff --git a/tests/unit/objects/test_estimate.py b/tests/unit/objects/test_estimate.py index 6794acd7..f88be666 100644 --- a/tests/unit/objects/test_estimate.py +++ b/tests/unit/objects/test_estimate.py @@ -9,7 +9,7 @@ def test_unicode(self): estimate = Estimate() estimate.TotalAmt = 10 - self.assertEquals(str(estimate), "10") + self.assertEqual(str(estimate), "10") def test_valid_object_name(self): obj = Estimate() diff --git a/tests/unit/objects/test_exchangerate.py b/tests/unit/objects/test_exchangerate.py new file mode 100644 index 00000000..15488dbc --- /dev/null +++ b/tests/unit/objects/test_exchangerate.py @@ -0,0 +1,23 @@ +import unittest + +from quickbooks import QuickBooks +from quickbooks.objects.exchangerate import ExchangeRate, ExchangeRateMetaData + + +class ExchangeRateTests(unittest.TestCase): + def test_unicode(self): + exchange_rate = ExchangeRate() + exchange_rate.SourceCurrencyCode = "EUR" + + exchange_rate.MetaData = ExchangeRateMetaData() + exchange_rate.MetaData.LastUpdatedTime = "1" + + self.assertEqual(str(exchange_rate), "EUR") + self.assertEqual(exchange_rate.MetaData.LastUpdatedTime, "1") + + def test_valid_object_name(self): + obj = ExchangeRate() + client = QuickBooks() + result = client.isvalid_object_name(obj.qbo_object_name) + + self.assertTrue(result) diff --git a/tests/unit/objects/test_invoice.py b/tests/unit/objects/test_invoice.py index 27b75a3c..1536fa35 100644 --- a/tests/unit/objects/test_invoice.py +++ b/tests/unit/objects/test_invoice.py @@ -10,7 +10,7 @@ def test_unicode(self): invoice = Invoice() invoice.TotalAmt = 10 - self.assertEquals(str(invoice), "10") + self.assertEqual(str(invoice), "10") def test_to_LinkedTxn(self): invoice = Invoice() @@ -19,9 +19,9 @@ def test_to_LinkedTxn(self): linked_txn = invoice.to_linked_txn() - self.assertEquals(linked_txn.TxnId, invoice.Id) - self.assertEquals(linked_txn.TxnType, "Invoice") - self.assertEquals(linked_txn.TxnLineId, 1) + self.assertEqual(linked_txn.TxnId, invoice.Id) + self.assertEqual(linked_txn.TxnType, "Invoice") + self.assertEqual(linked_txn.TxnLineId, 1) def test_email_sent_true(self): invoice = Invoice() @@ -47,14 +47,14 @@ def test_to_ref(self): ref = invoice.to_ref() self.assertIsInstance(ref, Ref) - self.assertEquals(ref.type, "Invoice") - self.assertEquals(ref.name, 1) # should be DocNumber - self.assertEquals(ref.value, 2) # should be Id + self.assertEqual(ref.type, "Invoice") + self.assertEqual(ref.name, 1) # should be DocNumber + self.assertEqual(ref.value, 2) # should be Id class DeliveryInfoTests(unittest.TestCase): def test_init(self): info = DeliveryInfo() - self.assertEquals(info.DeliveryType, "") - self.assertEquals(info.DeliveryTime, "") + self.assertEqual(info.DeliveryType, "") + self.assertEqual(info.DeliveryTime, "") diff --git a/tests/unit/objects/test_item.py b/tests/unit/objects/test_item.py index c4734d89..cba055f7 100644 --- a/tests/unit/objects/test_item.py +++ b/tests/unit/objects/test_item.py @@ -9,7 +9,7 @@ def test_unicode(self): item = Item() item.Name = "test" - self.assertEquals(str(item), "test") + self.assertEqual(str(item), "test") def test_to_ref(self): item = Item() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = item.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Item") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Item") + self.assertEqual(ref.value, 100) def test_valid_object_name(self): obj = Item() diff --git a/tests/unit/objects/test_journalentry.py b/tests/unit/objects/test_journalentry.py index cd1e9d47..7f32b300 100644 --- a/tests/unit/objects/test_journalentry.py +++ b/tests/unit/objects/test_journalentry.py @@ -9,7 +9,7 @@ def test_unicode(self): journalentry = JournalEntry() journalentry.TotalAmt = 1000 - self.assertEquals(str(journalentry), '1000') + self.assertEqual(str(journalentry), '1000') def test_valid_object_name(self): obj = JournalEntry() @@ -23,28 +23,28 @@ class JournalEntryLineTests(unittest.TestCase): def test_init(self): journalentry = JournalEntryLine() - self.assertEquals(journalentry.DetailType, "JournalEntryLineDetail") - self.assertEquals(journalentry.JournalEntryLineDetail, None) + self.assertEqual(journalentry.DetailType, "JournalEntryLineDetail") + self.assertEqual(journalentry.JournalEntryLineDetail, None) class JournalEntryLineDetailTests(unittest.TestCase): def test_init(self): journalentry = JournalEntryLineDetail() - self.assertEquals(journalentry.PostingType, "") - self.assertEquals(journalentry.TaxApplicableOn, "Sales") - self.assertEquals(journalentry.TaxAmount, 0) - self.assertEquals(journalentry.BillableStatus, None) - self.assertEquals(journalentry.Entity, None) - self.assertEquals(journalentry.AccountRef, None) - self.assertEquals(journalentry.ClassRef, None) - self.assertEquals(journalentry.DepartmentRef, None) - self.assertEquals(journalentry.TaxCodeRef, None) + self.assertEqual(journalentry.PostingType, "") + self.assertEqual(journalentry.TaxApplicableOn, "Sales") + self.assertEqual(journalentry.TaxAmount, 0) + self.assertEqual(journalentry.BillableStatus, None) + self.assertEqual(journalentry.Entity, None) + self.assertEqual(journalentry.AccountRef, None) + self.assertEqual(journalentry.ClassRef, None) + self.assertEqual(journalentry.DepartmentRef, None) + self.assertEqual(journalentry.TaxCodeRef, None) class EntityTests(unittest.TestCase): def test_init(self): entity = Entity() - self.assertEquals(entity.Type, "") - self.assertEquals(entity.EntityRef, None) + self.assertEqual(entity.Type, "") + self.assertEqual(entity.EntityRef, None) diff --git a/tests/unit/objects/test_payment.py b/tests/unit/objects/test_payment.py index d8be81c3..d397da4b 100644 --- a/tests/unit/objects/test_payment.py +++ b/tests/unit/objects/test_payment.py @@ -9,7 +9,7 @@ def test_unicode(self): payment_line = PaymentLine() payment_line.Amount = 100 - self.assertEquals(str(payment_line), "100") + self.assertEqual(str(payment_line), "100") class PaymentTests(unittest.TestCase): @@ -17,7 +17,7 @@ def test_unicode(self): payment = Payment() payment.TotalAmt = 1000 - self.assertEquals(str(payment), '1000') + self.assertEqual(str(payment), '1000') def test_valid_object_name(self): obj = Payment() diff --git a/tests/unit/objects/test_paymentmethod.py b/tests/unit/objects/test_paymentmethod.py index 59788088..d8aabb8b 100644 --- a/tests/unit/objects/test_paymentmethod.py +++ b/tests/unit/objects/test_paymentmethod.py @@ -9,7 +9,7 @@ def test_unicode(self): payment_method = PaymentMethod() payment_method.Name = "test" - self.assertEquals(str(payment_method), "test") + self.assertEqual(str(payment_method), "test") def test_valid_object_name(self): obj = PaymentMethod() @@ -25,6 +25,6 @@ def test_to_ref(self): ref = obj.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "PaymentMethod") - self.assertEquals(ref.value, 12) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "PaymentMethod") + self.assertEqual(ref.value, 12) diff --git a/tests/unit/objects/test_preferences.py b/tests/unit/objects/test_preferences.py new file mode 100644 index 00000000..85adb1ba --- /dev/null +++ b/tests/unit/objects/test_preferences.py @@ -0,0 +1,19 @@ +import unittest + +from quickbooks import QuickBooks +from quickbooks.objects.preferences import Preferences + + +class PreferencesTests(unittest.TestCase): + def test_unicode(self): + preferences = Preferences() + preferences.Id = 137 + + self.assertEqual(str(preferences), "Preferences 137") + + def test_valid_object_name(self): + preferences = Preferences() + client = QuickBooks() + result = client.isvalid_object_name(preferences.qbo_object_name) + + self.assertTrue(result) diff --git a/tests/unit/objects/test_purchase.py b/tests/unit/objects/test_purchase.py index 5ab8766c..5a9dc80f 100644 --- a/tests/unit/objects/test_purchase.py +++ b/tests/unit/objects/test_purchase.py @@ -9,7 +9,7 @@ def test_unicode(self): purchase = Purchase() purchase.TotalAmt = 1000 - self.assertEquals(str(purchase), "1000") + self.assertEqual(str(purchase), "1000") def test_valid_object_name(self): obj = Purchase() diff --git a/tests/unit/objects/test_purchaseorder.py b/tests/unit/objects/test_purchaseorder.py index 102ae393..4e63296b 100644 --- a/tests/unit/objects/test_purchaseorder.py +++ b/tests/unit/objects/test_purchaseorder.py @@ -9,7 +9,7 @@ def test_unicode(self): purchase_order = PurchaseOrder() purchase_order.TotalAmt = 1000 - self.assertEquals(str(purchase_order), '1000') + self.assertEqual(str(purchase_order), '1000') def test_valid_object_name(self): obj = PurchaseOrder() diff --git a/tests/unit/objects/test_recurringtransaction.py b/tests/unit/objects/test_recurringtransaction.py new file mode 100644 index 00000000..4320818d --- /dev/null +++ b/tests/unit/objects/test_recurringtransaction.py @@ -0,0 +1,29 @@ +import unittest + +from quickbooks import QuickBooks +from quickbooks.objects.recurringtransaction import RecurringTransaction, ScheduleInfo, RecurringInfo + + +class RecurringTransactionTests(unittest.TestCase): + def test_valid_object_name(self): + obj = RecurringTransaction() + client = QuickBooks() + result = client.isvalid_object_name(obj.qbo_object_name) + + self.assertTrue(result) + + +class ScheduleInfoTest(unittest.TestCase): + def test_create(self): + obj = ScheduleInfo() + obj.DayOfMonth = "1" + + self.assertEqual(obj.DayOfMonth, "1") + + +class RecurringInfoTest(unittest.TestCase): + def test_create(self): + obj = RecurringInfo() + + self.assertEqual(obj.RecurType, "Automated") + diff --git a/tests/unit/objects/test_refundreceipt.py b/tests/unit/objects/test_refundreceipt.py index c7da7f91..32fef70f 100644 --- a/tests/unit/objects/test_refundreceipt.py +++ b/tests/unit/objects/test_refundreceipt.py @@ -9,7 +9,7 @@ def test_unicode(self): deposit = RefundReceipt() deposit.TotalAmt = 100 - self.assertEquals(str(deposit), "100") + self.assertEqual(str(deposit), "100") def test_valid_object_name(self): obj = RefundReceipt() diff --git a/tests/unit/objects/test_salesreceipt.py b/tests/unit/objects/test_salesreceipt.py index 141dc48f..cdba115e 100644 --- a/tests/unit/objects/test_salesreceipt.py +++ b/tests/unit/objects/test_salesreceipt.py @@ -9,7 +9,7 @@ def test_unicode(self): sales_receipt = SalesReceipt() sales_receipt.TotalAmt = 100 - self.assertEquals(str(sales_receipt), "100") + self.assertEqual(str(sales_receipt), "100") def test_valid_object_name(self): obj = SalesReceipt() diff --git a/tests/unit/objects/test_tax.py b/tests/unit/objects/test_tax.py index 6acb7ef2..6f3f84f8 100644 --- a/tests/unit/objects/test_tax.py +++ b/tests/unit/objects/test_tax.py @@ -8,7 +8,7 @@ def test_unicode(self): detail = TaxLineDetail() detail.TaxPercent = 10 - self.assertEquals(str(detail), "10") + self.assertEqual(str(detail), "10") class TaxLineTests(unittest.TestCase): @@ -16,7 +16,7 @@ def test_unicode(self): line = TaxLine() line.Amount = 100 - self.assertEquals(str(line), "100") + self.assertEqual(str(line), "100") class TxnTaxDetailTests(unittest.TestCase): @@ -24,4 +24,4 @@ def test_unicode(self): detail = TxnTaxDetail() detail.TotalTax = 100 - self.assertEquals(str(detail), "100") + self.assertEqual(str(detail), "100") diff --git a/tests/unit/objects/test_taxagency.py b/tests/unit/objects/test_taxagency.py index 12bf32a3..4b5f3d51 100644 --- a/tests/unit/objects/test_taxagency.py +++ b/tests/unit/objects/test_taxagency.py @@ -8,4 +8,4 @@ def test_unicode(self): deposit = TaxAgency() deposit.DisplayName = "test" - self.assertEquals(str(deposit), "test") + self.assertEqual(str(deposit), "test") diff --git a/tests/unit/objects/test_taxcode.py b/tests/unit/objects/test_taxcode.py index 89828f23..371ce7f5 100644 --- a/tests/unit/objects/test_taxcode.py +++ b/tests/unit/objects/test_taxcode.py @@ -9,7 +9,7 @@ def test_unicode(self): taxcode = TaxCode() taxcode.Name = "test" - self.assertEquals(str(taxcode), "test") + self.assertEqual(str(taxcode), "test") def test_valid_object_name(self): obj = TaxCode() @@ -24,21 +24,21 @@ def test_to_ref(self): taxcode.Name = "test" ref = taxcode.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "TaxCode") - self.assertEquals(ref.value, 2) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "TaxCode") + self.assertEqual(ref.value, 2) class TaxRateDetailTests(unittest.TestCase): def test_init(self): tax_rate = TaxRateDetail() - self.assertEquals(tax_rate.TaxOrder, 0) - self.assertEquals(tax_rate.TaxTypeApplicable, "") + self.assertEqual(tax_rate.TaxOrder, 0) + self.assertEqual(tax_rate.TaxTypeApplicable, "") class TaxRateListTests(unittest.TestCase): def test_init(self): tax_rate_list = TaxRateList() - self.assertEquals(tax_rate_list.TaxRateDetail, []) + self.assertEqual(tax_rate_list.TaxRateDetail, []) diff --git a/tests/unit/objects/test_taxrate.py b/tests/unit/objects/test_taxrate.py index 50a7a91c..91dfc295 100644 --- a/tests/unit/objects/test_taxrate.py +++ b/tests/unit/objects/test_taxrate.py @@ -9,7 +9,7 @@ def test_unicode(self): tax = TaxRate() tax.Name = "test" - self.assertEquals(str(tax), "test") + self.assertEqual(str(tax), "test") def test_valid_object_name(self): obj = TaxRate() diff --git a/tests/unit/objects/test_taxservice.py b/tests/unit/objects/test_taxservice.py index 7a8f3efd..9ec56402 100644 --- a/tests/unit/objects/test_taxservice.py +++ b/tests/unit/objects/test_taxservice.py @@ -8,7 +8,7 @@ def test_unicode(self): tax = TaxService() tax.TaxCode = "test" - self.assertEquals(str(tax), "test") + self.assertEqual(str(tax), "test") class TaxRateDetailsTests(unittest.TestCase): @@ -16,4 +16,4 @@ def test_unicode(self): tax = TaxRateDetails() tax.TaxRateName = "test" - self.assertEquals(str(tax), "test") \ No newline at end of file + self.assertEqual(str(tax), "test") \ No newline at end of file diff --git a/tests/unit/objects/test_term.py b/tests/unit/objects/test_term.py index 57c9f7b1..31d98779 100644 --- a/tests/unit/objects/test_term.py +++ b/tests/unit/objects/test_term.py @@ -9,7 +9,7 @@ def test_unicode(self): term = Term() term.Name = "test" - self.assertEquals(str(term), "test") + self.assertEqual(str(term), "test") def test_valid_object_name(self): obj = Term() @@ -25,6 +25,6 @@ def test_to_ref(self): ref = term.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Term") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Term") + self.assertEqual(ref.value, 100) diff --git a/tests/unit/objects/test_timeactivity.py b/tests/unit/objects/test_timeactivity.py index 9663de21..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,17 +17,18 @@ def test_unicode(self): time_activity.BreakHours = 1 time_activity.BreakMinutes = 60 time_activity.Description = "test" + time_activity.CostRate = 50.0 - self.assertEquals(str(time_activity), "test") - self.assertEquals(time_activity.TimeZone, "CST") - self.assertEquals(time_activity.BillableStatus, "test") - self.assertEquals(time_activity.Taxable, False) - self.assertEquals(time_activity.HourlyRate, 0) - self.assertEquals(time_activity.Hours, 1) - self.assertEquals(time_activity.Minutes, 60) - self.assertEquals(time_activity.BreakHours, 1) - self.assertEquals(time_activity.BreakMinutes, 60) - self.assertEquals(time_activity.Description, "test") + self.assertEqual(str(time_activity), "test") + self.assertEqual(time_activity.BillableStatus, "test") + self.assertEqual(time_activity.Taxable, False) + self.assertEqual(time_activity.HourlyRate, 0) + self.assertEqual(time_activity.Hours, 1) + self.assertEqual(time_activity.Minutes, 60) + 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/objects/test_trackingclass.py b/tests/unit/objects/test_trackingclass.py index 4b351d70..b397e08a 100644 --- a/tests/unit/objects/test_trackingclass.py +++ b/tests/unit/objects/test_trackingclass.py @@ -8,7 +8,7 @@ def test_unicode(self): cls = Class() cls.Name = "test" - self.assertEquals(str(cls), "test") + self.assertEqual(str(cls), "test") def test_to_ref(self): cls = Class() @@ -17,6 +17,6 @@ def test_to_ref(self): dept_ref = cls.to_ref() - self.assertEquals(dept_ref.name, "test") - self.assertEquals(dept_ref.type, "Class") - self.assertEquals(dept_ref.value, 100) + self.assertEqual(dept_ref.name, "test") + self.assertEqual(dept_ref.type, "Class") + self.assertEqual(dept_ref.value, 100) diff --git a/tests/unit/objects/test_transfer.py b/tests/unit/objects/test_transfer.py index f93bc56e..9ce2da1f 100644 --- a/tests/unit/objects/test_transfer.py +++ b/tests/unit/objects/test_transfer.py @@ -9,7 +9,7 @@ def test_unicode(self): transfer = Transfer() transfer.Amount = 100 - self.assertEquals(str(transfer), "100") + self.assertEqual(str(transfer), "100") def test_valid_object_name(self): obj = Transfer() diff --git a/tests/unit/objects/test_vendor.py b/tests/unit/objects/test_vendor.py index db7864d3..d4678420 100644 --- a/tests/unit/objects/test_vendor.py +++ b/tests/unit/objects/test_vendor.py @@ -9,7 +9,7 @@ def test_unicode(self): vendor = Vendor() vendor.DisplayName = "test" - self.assertEquals(str(vendor), "test") + self.assertEqual(str(vendor), "test") def test_to_ref(self): vendor = Vendor() @@ -18,9 +18,9 @@ def test_to_ref(self): ref = vendor.to_ref() - self.assertEquals(ref.name, "test") - self.assertEquals(ref.type, "Vendor") - self.assertEquals(ref.value, 100) + self.assertEqual(ref.name, "test") + self.assertEqual(ref.type, "Vendor") + self.assertEqual(ref.value, 100) def test_valid_object_name(self): obj = Vendor() @@ -34,5 +34,5 @@ class ContactInfoTests(unittest.TestCase): def test_init(self): contact_info = ContactInfo() - self.assertEquals(contact_info.Type, "") - self.assertEquals(contact_info.Telephone, None) + self.assertEqual(contact_info.Type, "") + self.assertEqual(contact_info.Telephone, None) diff --git a/tests/unit/objects/test_vendorcredit.py b/tests/unit/objects/test_vendorcredit.py index c3631e12..cabaed54 100644 --- a/tests/unit/objects/test_vendorcredit.py +++ b/tests/unit/objects/test_vendorcredit.py @@ -9,7 +9,7 @@ def test_unicode(self): vendor_credit = VendorCredit() vendor_credit.TotalAmt = 1000 - self.assertEquals(str(vendor_credit), "1000") + self.assertEqual(str(vendor_credit), "1000") def test_valid_object_name(self): obj = VendorCredit() diff --git a/tests/unit/test_batch.py b/tests/unit/test_batch.py index d8b762f0..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 @@ -49,13 +46,13 @@ def test_list_to_batch_request(self): obj_list = [self.object1, self.object2] batch_request = batch_mgr.list_to_batch_request(obj_list) - self.assertEquals(len(batch_request.BatchItemRequest), 2) + self.assertEqual(len(batch_request.BatchItemRequest), 2) batch_item = batch_request.BatchItemRequest[0] self.assertTrue(batch_item.bId) self.assertTrue(len(batch_item.bId) < 50) - self.assertEquals(batch_item.operation, "create") - self.assertEquals(batch_item.get_object(), self.object1) + self.assertEqual(batch_item.operation, "create") + self.assertEqual(batch_item.get_object(), self.object1) def test_batch_results_to_list(self): batch_mgr = batch.BatchManager("create") @@ -72,5 +69,5 @@ def test_batch_results_to_list(self): results = batch_mgr.batch_results_to_list(json_data, batch_request, self.obj_list) - self.assertEquals(len(results.faults), 1) - self.assertEquals(len(results.successes), 1) + self.assertEqual(len(results.faults), 1) + self.assertEqual(len(results.successes), 1) diff --git a/tests/unit/test_cdc.py b/tests/unit/test_cdc.py index df3d122c..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 @@ -89,16 +86,16 @@ def setUp(self): def test_change_data_capture(self, make_request): make_request.return_value = self.cdc_json_response.copy() cdc_response = change_data_capture([Invoice, Customer], "2017-01-01T00:00:00") - self.assertEquals(1, len(cdc_response.Customer)) - self.assertEquals(2, len(cdc_response.Invoice)) + self.assertEqual(1, len(cdc_response.Customer)) + self.assertEqual(2, len(cdc_response.Invoice)) @patch('quickbooks.client.QuickBooks.make_request') def test_change_data_capture_with_timestamp(self, make_request): make_request.return_value = self.cdc_json_response.copy() cdc_response_with_datetime = change_data_capture([Invoice, Customer], datetime(2017, 1, 1, 0, 0, 0)) - self.assertEquals(1, len(cdc_response_with_datetime.Customer)) - self.assertEquals(2, len(cdc_response_with_datetime.Invoice)) + self.assertEqual(1, len(cdc_response_with_datetime.Customer)) + self.assertEqual(2, len(cdc_response_with_datetime.Invoice)) @patch('quickbooks.client.QuickBooks.make_request') def test_change_data_capture_with_empty_response(self, make_request): diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 69547366..94b1eff7 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,12 +1,10 @@ +import json +import warnings from tests.integration.test_base import QuickbooksUnitTestCase - -try: - from mock import patch -except ImportError: - from unittest.mock import patch +from unittest.mock import patch, mock_open from quickbooks.exceptions import QuickbooksException, SevereException, AuthorizationException -from quickbooks import client +from quickbooks import client, mixins from quickbooks.objects.salesreceipt import SalesReceipt @@ -17,8 +15,12 @@ class ClientTest(QuickbooksUnitTestCase): + def setUp(self): + super(ClientTest, self).setUp() + + self.auth_client.access_token = 'ACCESS_TOKEN' + def tearDown(self): - client.QuickBooks.enable_global() self.qb_client = client.QuickBooks() self.qb_client._drop() @@ -26,37 +28,27 @@ def test_client_new(self): self.qb_client = client.QuickBooks( company_id="company_id", verbose=True, - minorversion=4, verifier_token=TEST_VERIFIER_TOKEN, ) - self.assertEquals(self.qb_client.company_id, "company_id") - self.assertEquals(self.qb_client.minorversion, 4) - - def test_client_updated(self): - self.qb_client = client.QuickBooks( - sandbox=False, - company_id="company_id", - ) - - self.qb_client2 = client.QuickBooks( - sandbox=True, - company_id="update_company_id", - ) - - self.assertEquals(self.qb_client.sandbox, True) - self.assertEquals(self.qb_client.company_id, "update_company_id") - - self.assertEquals(self.qb_client2.sandbox, True) - self.assertEquals(self.qb_client2.company_id, "update_company_id") - - def test_disable_global(self): - client.QuickBooks.disable_global() - self.qb_client = client.QuickBooks() - - self.assertFalse(self.qb_client.sandbox) - self.assertFalse(self.qb_client.company_id) - self.assertFalse(self.qb_client.minorversion) + self.assertEqual(self.qb_client.company_id, "company_id") + + def test_client_with_deprecated_minor_version(self): + with warnings.catch_warnings(record=True) as w: + self.qb_client = client.QuickBooks( + company_id="company_id", + verbose=True, + minorversion=74, + verifier_token=TEST_VERIFIER_TOKEN, + ) + + warnings.simplefilter("always") + self.assertEqual(self.qb_client.company_id, "company_id") + self.assertEqual(self.qb_client.minorversion, 74) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + 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(http://github.com/self): qb_client = client.QuickBooks(sandbox=False) @@ -65,8 +57,14 @@ def test_api_url(http://github.com/self): self.assertFalse("sandbox" in api_url) def test_api_url_sandbox(self): - qb_client = client.QuickBooks(sandbox=True) + qb_client = client.QuickBooks( + auth_client=self.auth_client, + refresh_token='REFRESH_TOKEN', + company_id='COMPANY_ID', + ) + api_url = qb_client.api_url + print(api_url) self.assertTrue("sandbox" in api_url) @@ -74,7 +72,7 @@ def test_isvalid_object_name_valid(self): qb_client = client.QuickBooks() result = qb_client.isvalid_object_name("Customer") - self.assertEquals(result, True) + self.assertEqual(result, True) def test_isvalid_object_name_invalid(self): qb_client = client.QuickBooks() @@ -90,7 +88,8 @@ def test_batch_operation(self, make_req): @patch('quickbooks.client.QuickBooks.post') def test_misc_operation(self, post): - qb_client = client.QuickBooks() + qb_client = client.QuickBooks(company_id='COMPANY_ID', auth_client=self.auth_client) + qb_client.misc_operation("end_point", "request_body") url = "https://sandbox-quickbooks.api.intuit.com/v3/company/COMPANY_ID/end_point" @@ -117,6 +116,15 @@ def test_update_object(self, post): self.assertTrue(post.called) + @patch('quickbooks.client.QuickBooks.make_request') + def test_update_object_with_request_id(self, make_req): + qb_client = client.QuickBooks(auth_client=self.auth_client) + qb_client.company_id = "1234" + 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, file_bytes=None, request_id="123", params={}) + @patch('quickbooks.client.QuickBooks.get') def test_get_current_user(self, get): qb_client = client.QuickBooks() @@ -128,31 +136,35 @@ def test_get_current_user(self, get): @patch('quickbooks.client.QuickBooks.make_request') def test_get_report(self, make_req): - qb_client = client.QuickBooks() + qb_client = client.QuickBooks(auth_client=self.auth_client) qb_client.company_id = "1234" 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}) - - def test_get_instance(self): - qb_client = client.QuickBooks() - - instance = qb_client.get_instance() - self.assertEquals(qb_client, instance) + 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): - qb_client = client.QuickBooks() + qb_client = client.QuickBooks(auth_client=self.auth_client) 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, {}) + 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): + qb_client = client.QuickBooks(auth_client=self.auth_client) + 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" + make_req.assert_called_with("GET", url, {}, params={'param':'value'}) @patch('quickbooks.client.QuickBooks.process_request') def test_make_request(self, process_request): - process_request.return_value = MockResponse() + process_request.return_value = MockResponseJson() qb_client = client.QuickBooks() qb_client.company_id = "1234" @@ -161,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() @@ -228,11 +241,27 @@ def test_download_pdf_not_authorized(self, process_request): self.assertRaises(AuthorizationException, receipt.download_pdf, self.qb_client) + @patch('quickbooks.client.QuickBooks.process_request') + def test_make_request_file_closed(self, process_request): + file_path = '/path/to/file.txt' + process_request.return_value = MockResponseJson() + with patch('builtins.open', mock_open(read_data=b'file content')) as mock_file: + qb_client = client.QuickBooks(auth_client=self.auth_client) + qb_client.make_request('POST', + 'https://sandbox-quickbooks.api.intuit.com/v3/company/COMPANY_ID/attachable', + request_body='{"ContentType": "text/plain"}', + file_path=file_path) + + mock_file.assert_called_once_with(file_path, 'rb') + mock_file.return_value.__enter__.return_value.read.assert_called_once() + mock_file.return_value.__exit__.assert_called_once() + process_request.assert_called_once() + 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): @@ -243,10 +272,20 @@ 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): + self.json_data = json_data or {} + self.status_code = status_code + + @property + def text(self): + return json.dumps(self.json_data, cls=mixins.DecimalEncoder) + + def json(self): + return self.json_data class MockUnauthorizedResponse(object): @@ -283,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_decimal.py b/tests/unit/test_decimal.py new file mode 100644 index 00000000..77feccae --- /dev/null +++ b/tests/unit/test_decimal.py @@ -0,0 +1,21 @@ +from decimal import Decimal +import unittest +from quickbooks.objects.bill import Bill +from quickbooks.objects.detailline import DetailLine + + +class DecimalTestCase(unittest.TestCase): + def test_bill_with_decimal_amount(self): + """Test that a Bill with decimal line amounts can be converted to JSON without errors""" + bill = Bill() + line = DetailLine() + line.Amount = Decimal('42.42') + line.DetailType = "AccountBasedExpenseLineDetail" + + bill.Line.append(line) + + # This should not raise any exceptions + json_data = bill.to_json() + + # Verify the amount was converted correctly + self.assertIn('"Amount": "42.42"', json_data) diff --git a/tests/unit/test_exception.py b/tests/unit/test_exception.py index 25b2fd8a..393d6a2c 100644 --- a/tests/unit/test_exception.py +++ b/tests/unit/test_exception.py @@ -8,13 +8,13 @@ class QuickbooksExceptionTests(unittest.TestCase): def test_init(self): exception = QuickbooksException("message", 100, "detail") - self.assertEquals(exception.message, "message") - self.assertEquals(exception.error_code, 100) - self.assertEquals(exception.detail, "detail") + self.assertEqual(exception.message, "message") + self.assertEqual(exception.error_code, 100) + self.assertEqual(exception.detail, "detail") class AuthorizationExceptionTests(unittest.TestCase): def test_unicode(self): exception = AuthorizationException("message", detail="detail") - self.assertEquals(str(exception), "QB Auth Exception: message \n\ndetail") + self.assertEqual(str(exception), "QB Auth Exception 0: message\ndetail") diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 90459864..e90fc47a 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -7,12 +7,12 @@ class HelpersTests(unittest.TestCase): def test_qb_date_format(self): result = qb_date_format(date(2016, 7, 22)) - self.assertEquals(result, '2016-07-22') + self.assertEqual(result, '2016-07-22') def test_qb_datetime_format(self): result = qb_datetime_format(datetime(2016, 7, 22, 10, 35, 00)) - self.assertEquals(result, '2016-07-22T10:35:00') + self.assertEqual(result, '2016-07-22T10:35:00') def test_qb_datetime_utc_offset_format(self): result = qb_datetime_utc_offset_format(datetime(2016, 7, 22, 10, 35, 00), '-06:00') - self.assertEquals(result, '2016-07-22T10:35:00-06:00') + self.assertEqual(result, '2016-07-22T10:35:00-06:00') diff --git a/tests/unit/test_mixins.py b/tests/unit/test_mixins.py index 42765538..d2d6fc31 100644 --- a/tests/unit/test_mixins.py +++ b/tests/unit/test_mixins.py @@ -1,24 +1,19 @@ - -import os - import unittest -from future.moves.urllib.parse import quote +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 +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 quickbooks import client +from tests.unit.test_client import MockSession from quickbooks.objects.base import PhoneNumber, QuickbooksBaseObject from quickbooks.objects.department import Department from quickbooks.objects.customer import Customer from quickbooks.objects.journalentry import JournalEntry, JournalEntryLine +from quickbooks.objects.recurringtransaction import RecurringTransaction from quickbooks.objects.salesreceipt import SalesReceipt from quickbooks.mixins import ObjectListMixin @@ -30,7 +25,7 @@ def test_to_json(self): json = phone.to_json() - self.assertEquals(json, '{\n "FreeFormNumber": "555-555-5555"\n}') + self.assertEqual(json, '{\n "FreeFormNumber": "555-555-5555"\n}') class FromJsonMixinTest(unittest.TestCase): @@ -55,25 +50,25 @@ def test_from_json(self): entry = JournalEntry() new_obj = entry.from_json(self.json_data) - self.assertEquals(type(new_obj), JournalEntry) - self.assertEquals(new_obj.DocNumber, "123") - self.assertEquals(new_obj.TotalAmt, 100) + self.assertEqual(type(new_obj), JournalEntry) + self.assertEqual(new_obj.DocNumber, "123") + self.assertEqual(new_obj.TotalAmt, 100) line = new_obj.Line[0] - self.assertEquals(type(line), JournalEntryLine) - self.assertEquals(line.Description, "Test") - self.assertEquals(line.Amount, 25.54) - self.assertEquals(line.DetailType, "JournalEntryLineDetail") - self.assertEquals(line.JournalEntryLineDetail.PostingType, "Debit") + self.assertEqual(type(line), JournalEntryLine) + self.assertEqual(line.Description, "Test") + self.assertEqual(line.Amount, 25.54) + self.assertEqual(line.DetailType, "JournalEntryLineDetail") + self.assertEqual(line.JournalEntryLineDetail.PostingType, "Debit") def test_from_json_missing_detail_object(self): test_obj = QuickbooksBaseObject() new_obj = test_obj.from_json(self.json_data) - self.assertEquals(type(new_obj), QuickbooksBaseObject) - self.assertEquals(new_obj.DocNumber, "123") - self.assertEquals(new_obj.TotalAmt, 100) + self.assertEqual(type(new_obj), QuickbooksBaseObject) + self.assertEqual(new_obj.DocNumber, "123") + self.assertEqual(new_obj.TotalAmt, 100) class ToDictMixinTest(unittest.TestCase): @@ -130,19 +125,21 @@ def test_to_dict(self): 'TxnTaxDetail': None, } - self.assertEquals(expected, entry.to_dict()) + self.assertEqual(expected, entry.to_dict()) 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): @@ -214,7 +211,7 @@ class ReadMixinTest(QuickbooksUnitTestCase): @patch('quickbooks.mixins.QuickBooks.get_single_object') def test_get(self, get_single_object): Department.get(1) - get_single_object.assert_called_once_with("Department", pk=1) + get_single_object.assert_called_once_with("Department", pk=1, params=None) def test_get_with_qb(self): with patch.object(self.qb_client, 'get_single_object') as get_single_object: @@ -227,7 +224,7 @@ class UpdateMixinTest(QuickbooksUnitTestCase): def test_save_create(self, create_object): department = Department() department.save(qb=self.qb_client) - create_object.assert_called_once_with("Department", department.to_json()) + create_object.assert_called_once_with("Department", department.to_json(), request_id=None, params=None) def test_save_create_with_qb(self): with patch.object(self.qb_client, 'create_object') as create_object: @@ -242,7 +239,7 @@ def test_save_update(self, update_object): json = department.to_json() department.save(qb=self.qb_client) - update_object.assert_called_once_with("Department", json) + update_object.assert_called_once_with("Department", json, request_id=None, params=None) def test_save_update_with_qb(self): with patch.object(self.qb_client, 'update_object') as update_object: @@ -285,50 +282,50 @@ def test_object_list_mixin_with_primitives(self): test_primitive_list = [1, 2, 3] test_subclass_primitive_obj = self.TestSubclass(test_primitive_list) - self.assertEquals(test_primitive_list, test_subclass_primitive_obj[:]) + self.assertEqual(test_primitive_list, test_subclass_primitive_obj[:]) for index in range(0, len(test_subclass_primitive_obj)): - self.assertEquals(test_primitive_list[index], test_subclass_primitive_obj[index]) + self.assertEqual(test_primitive_list[index], test_subclass_primitive_obj[index]) for prim in test_subclass_primitive_obj: - self.assertEquals(True, prim in test_subclass_primitive_obj) + self.assertEqual(True, prim in test_subclass_primitive_obj) - self.assertEquals(3, test_subclass_primitive_obj.pop()) + self.assertEqual(3, test_subclass_primitive_obj.pop()) test_subclass_primitive_obj.append(4) - self.assertEquals([1, 2, 4], test_subclass_primitive_obj[:]) + self.assertEqual([1, 2, 4], test_subclass_primitive_obj[:]) test_subclass_primitive_obj[0] = 5 - self.assertEquals([5, 2, 4], test_subclass_primitive_obj[:]) + self.assertEqual([5, 2, 4], test_subclass_primitive_obj[:]) del test_subclass_primitive_obj[0] - self.assertEquals([2, 4], test_subclass_primitive_obj[:]) + self.assertEqual([2, 4], test_subclass_primitive_obj[:]) - self.assertEquals([4, 2], list(reversed(test_subclass_primitive_obj))) + self.assertEqual([4, 2], list(reversed(test_subclass_primitive_obj))) def test_object_list_mixin_with_qb_objects(self): pn1, pn2, pn3, pn4, pn5 = PhoneNumber(), PhoneNumber(), PhoneNumber(), PhoneNumber(), PhoneNumber() test_object_list = [pn1, pn2, pn3] test_subclass_object_obj = self.TestSubclass(test_object_list) - self.assertEquals(test_object_list, test_subclass_object_obj[:]) + self.assertEqual(test_object_list, test_subclass_object_obj[:]) for index in range (0, len(test_subclass_object_obj)): - self.assertEquals(test_object_list[index], test_subclass_object_obj[index]) + self.assertEqual(test_object_list[index], test_subclass_object_obj[index]) for obj in test_subclass_object_obj: - self.assertEquals(True, obj in test_subclass_object_obj) + self.assertEqual(True, obj in test_subclass_object_obj) - self.assertEquals(pn3, test_subclass_object_obj.pop()) + self.assertEqual(pn3, test_subclass_object_obj.pop()) test_subclass_object_obj.append(pn4) - self.assertEquals([pn1, pn2, pn4], test_subclass_object_obj[:]) + self.assertEqual([pn1, pn2, pn4], test_subclass_object_obj[:]) test_subclass_object_obj[0] = pn5 - self.assertEquals([pn5, pn2, pn4], test_subclass_object_obj[:]) + self.assertEqual([pn5, pn2, pn4], test_subclass_object_obj[:]) del test_subclass_object_obj[0] - self.assertEquals([pn2, pn4], test_subclass_object_obj[:]) + self.assertEqual([pn2, pn4], test_subclass_object_obj[:]) - self.assertEquals([pn4, pn2], list(reversed(test_subclass_object_obj))) + self.assertEqual([pn4, pn2], list(reversed(test_subclass_object_obj))) class DeleteMixinTest(QuickbooksUnitTestCase): @@ -347,6 +344,16 @@ def test_delete(self, delete_object): self.assertTrue(delete_object.called) +class DeleteNoIdMixinTest(QuickbooksUnitTestCase): + @patch('quickbooks.mixins.QuickBooks.delete_object') + def test_delete(self, delete_object): + recurring_txn = RecurringTransaction() + recurring_txn.Bill = Bill() + recurring_txn.delete(qb=self.qb_client) + + self.assertTrue(delete_object.called) + + class SendMixinTest(QuickbooksUnitTestCase): @patch('quickbooks.mixins.QuickBooks.misc_operation') def test_send(self, mock_misc_op): @@ -370,12 +377,33 @@ def test_send_with_send_to_email(self, mock_misc_op): class VoidMixinTest(QuickbooksUnitTestCase): @patch('quickbooks.mixins.QuickBooks.post') - def test_void(self, post): + def test_void_invoice(self, post): invoice = Invoice() invoice.Id = 2 invoice.void(qb=self.qb_client) self.assertTrue(post.called) + @patch('quickbooks.mixins.QuickBooks.post') + def test_void_payment(self, post): + payment = Payment() + payment.Id = 2 + payment.void(qb=self.qb_client) + self.assertTrue(post.called) + + @patch('quickbooks.mixins.QuickBooks.post') + def test_void_sales_receipt(self, post): + sales_receipt = SalesReceipt() + sales_receipt.Id = 2 + sales_receipt.void(qb=self.qb_client) + self.assertTrue(post.called) + + @patch('quickbooks.mixins.QuickBooks.post') + def test_void_bill_payment(self, post): + bill_payment = BillPayment() + bill_payment.Id = 2 + bill_payment.void(qb=self.qb_client) + self.assertTrue(post.called) + def test_delete_unsaved_exception(self): from quickbooks.exceptions import QuickbooksException