From 5df50f68835a6541d19bbbbf2ec4e8725b21fd99 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 16:19:13 -0500 Subject: [PATCH 01/30] Remove enum34 from requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9a64921..034db61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ psycopg2>=2.6.2,<3 Django>=1.11,<3 -enum34 From 0666930173f372c3349a014743baf135e8847183 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:08:40 -0500 Subject: [PATCH 02/30] Remove Python 2 compatibility code --- capone/models.py | 6 ------ capone/tests/test_transaction_model.py | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/capone/models.py b/capone/models.py index 9d3fa70..8a2241a 100644 --- a/capone/models.py +++ b/capone/models.py @@ -10,7 +10,6 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import Q -from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from capone.exceptions import TransactionBalanceException @@ -20,7 +19,6 @@ NEGATIVE_DEBITS_HELP_TEXT = "Amount for this entry. Debits are negative, and credits are positive." # noqa: E501 -@python_2_unicode_compatible class TransactionRelatedObject(models.Model): """ A piece of evidence for a particular Transaction. @@ -164,7 +162,6 @@ def filter_by_related_objects( raise ValueError("Invalid match_type.") -@python_2_unicode_compatible class TransactionType(models.Model): """ A user-defined "type" to group `Transactions`. @@ -202,7 +199,6 @@ def get_or_create_manual_transaction_type_id(): return get_or_create_manual_transaction_type().id -@python_2_unicode_compatible class Transaction(models.Model): """ The main model for representing a financial event in `capone`. @@ -292,7 +288,6 @@ def summary(self): } -@python_2_unicode_compatible class Ledger(models.Model): """ A group of `LedgerEntries` all debiting or crediting the same resource. @@ -368,7 +363,6 @@ def __str__(self): amount=self.amount, ledger=self.ledger.name) -@python_2_unicode_compatible class LedgerBalance(models.Model): """ A Denormalized balance for a related object in a ledger. diff --git a/capone/tests/test_transaction_model.py b/capone/tests/test_transaction_model.py index 307a620..3d41ba7 100644 --- a/capone/tests/test_transaction_model.py +++ b/capone/tests/test_transaction_model.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import sys from datetime import datetime from decimal import Decimal @@ -56,10 +55,7 @@ def test_unicode_methods(self): ledger = LedgerFactory(name='foo') self.assertEqual(str(ledger), "Ledger foo") ledger = LedgerFactory(name='föo') - if sys.version_info.major == 2: - str(ledger) == b"Ledger f\xc3\xb6o" - if sys.version_info.major == 3: - self.assertTrue(str(ledger) == "Ledger föo") + self.assertTrue(str(ledger) == "Ledger föo") ttype = TransactionTypeFactory(name='foo') self.assertEqual(str(ttype), "Transaction Type foo") From 7b8783d4bcc1abed88c039053654bce38233ddd0 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:09:21 -0500 Subject: [PATCH 03/30] Update deprecated import --- capone/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capone/models.py b/capone/models.py index 8a2241a..45d5e1e 100644 --- a/capone/models.py +++ b/capone/models.py @@ -10,7 +10,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from capone.exceptions import TransactionBalanceException From 5925dcef78d376ab3b999c1e15d582b62c23a75c Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:09:34 -0500 Subject: [PATCH 04/30] Fix bug in 4.2: can't access related managers if no PK --- capone/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/capone/models.py b/capone/models.py index 45d5e1e..abad1e1 100644 --- a/capone/models.py +++ b/capone/models.py @@ -263,10 +263,11 @@ def validate(self): Instead, the only check that makes sense is that the entries for the transaction still balance. """ - total = sum([entry.amount for entry in self.entries.all()]) - if total != Decimal(0): - raise TransactionBalanceException( - "Credits do not equal debits. Mis-match of %s." % total) + if self.pk: + total = sum([entry.amount for entry in self.entries.all()]) + if total != Decimal(0): + raise TransactionBalanceException( + "Credits do not equal debits. Mis-match of %s." % total) return True def save(self, **kwargs): From 60e2ccf02701ba5cdda13a0108fb5786d32e05ad Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:09:57 -0500 Subject: [PATCH 05/30] Update to new import path --- capone/tests/factories.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/capone/tests/factories.py b/capone/tests/factories.py index 10a253b..9686cb6 100644 --- a/capone/tests/factories.py +++ b/capone/tests/factories.py @@ -13,7 +13,7 @@ from capone.tests.models import Order -class UserFactory(factory.DjangoModelFactory): +class UserFactory(factory.django.DjangoModelFactory): """ Factory for django.contrib.auth.get_user_model() @@ -27,7 +27,7 @@ class Meta: email = username = factory.Sequence(lambda n: "TransactionUser #%s" % n) -class LedgerFactory(factory.DjangoModelFactory): +class LedgerFactory(factory.django.DjangoModelFactory): class Meta: model = Ledger @@ -36,7 +36,7 @@ class Meta: number = factory.Sequence(lambda n: n) -class OrderFactory(factory.DjangoModelFactory): +class OrderFactory(factory.django.DjangoModelFactory): class Meta: model = Order @@ -44,14 +44,14 @@ class Meta: barcode = factory.Sequence(lambda n: str(n)) -class CreditCardTransactionFactory(factory.DjangoModelFactory): +class CreditCardTransactionFactory(factory.django.DjangoModelFactory): class Meta: model = CreditCardTransaction cardholder_name = factory.Sequence(lambda n: "Cardholder %s" % n) -class TransactionTypeFactory(factory.DjangoModelFactory): +class TransactionTypeFactory(factory.django.DjangoModelFactory): class Meta: model = TransactionType From 131f509a8de81073d8154a5c6afec42478822057 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:10:15 -0500 Subject: [PATCH 06/30] Unpin dependencies --- requirements-dev.txt | 12 ++++++------ requirements-setup.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2e89234..bd127f1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,8 @@ # Requirements needed for running the test suite. -factory_boy>=2,<3 -flake8>=3,<4 -parameterized>=0.7.1,<1 -pytest>=5,<6 -pytest-cov>=2,<3 -pytest-django>=3,<4 +factory_boy +flake8 +parameterized +pytest +pytest-cov +pytest-django diff --git a/requirements-setup.txt b/requirements-setup.txt index a699d5d..182561e 100644 --- a/requirements-setup.txt +++ b/requirements-setup.txt @@ -1,4 +1,4 @@ # Requirements needed for the setup make target and the test target # outside of the individual tox environments. -tox==3.14.6 +tox From 8cc99b26a13ebdd3c93e10a26639556f34a5736b Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:10:36 -0500 Subject: [PATCH 07/30] Update setup.cfg settings format --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1129a29..c55f01b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,10 @@ [metadata] name = capone author = Hunter Richards -author-email = opensource@counsyl.com +author_email = opensource@counsyl.com summary = Django app representing a double-entry accounting ledger. -description-file = README.rst -home-page = https://github.com/counsyl/capone +description_file = README.rst +home_page = https://github.com/counsyl/capone license = Copyright Counsyl, Inc. classifier = Development Status :: 5 - Production/Stable From 255f36ef5582ec5fac85f4f393f76fb7d1f7c862 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 09:09:49 -0500 Subject: [PATCH 08/30] Update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 907f9cc..d245f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 3.1.0 + +- No functional changes: added support targets and refactored tests and dependencies. + +## Major + +- Support Django 3.2 and 4.2. +- Add support for Python 3.8 and 3.9. +- Cope with psycopg2 bug: only 2.8 is supported through Django 2.2, but 2.9 is supported for higher Django versions. +- Parameterize test suite on USE_TZ to confirm Capone works for both True and False. +- Remove unneeded dependency `enum34`. +- Refactor to use `django.utils.timezone`. + +## Minor + +- Convert tests to Pytest style. +- Default in tests to USE_TZ == True. +- Remove remaining Python 2 compatibility shims. +- Unpin lots of test dependencies. + # 3.0.0 - Drop Django < 1.11 support. From a613137877dfc7282f20766e6f2a557e206b1c9a Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:10:26 -0500 Subject: [PATCH 09/30] Add more Python and Django versions --- requirements.txt | 2 +- tox.ini | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 034db61..aefd0b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ psycopg2>=2.6.2,<3 -Django>=1.11,<3 +Django>=1.11,<5 diff --git a/tox.ini b/tox.ini index 7c72b00..700dc10 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] install_command = pip install {opts} {packages} -envlist = py36-{1.11,2.2}, flake8 +envlist = {py36,py38,py39}-{1.11,2.2,3.2,4.2}, flake8 [testenv] passenv = LANG POSTGRES_HOST POSTGRES_DB POSTGRES_PASSWORD POSTGRES_PORT POSTGRES_USER @@ -22,6 +22,8 @@ deps = -r{toxinidir}/requirements-dev.txt 1.11: Django>=1.11,<2 2.2: Django>=2.2,<3 + 3.2: Django>=3.2,<4 + 4.2: Django>=3.2,<5 [testenv:flake8] commands = {envbindir}/flake8 -v capone From 868a4bd1ba90b02371d262cef3cad778778497f9 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:18:34 -0500 Subject: [PATCH 10/30] Add supported Python versions --- .python-version | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..807c3e5 --- /dev/null +++ b/.python-version @@ -0,0 +1,3 @@ +3.6 +3.8 +3.9 From 603dd2b0926bb0f59eba0b774d362ac949f170e7 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 17:18:17 -0500 Subject: [PATCH 11/30] Use django.utils.timezone --- capone/tests/settings.py | 2 ++ ...sert_transaction_in_ledgers_for_amounts_with_evidence.py | 6 +++--- capone/tests/test_create_transaction.py | 4 ++-- capone/tests/test_factories.py | 4 ++-- capone/tests/test_transaction_model.py | 6 +++--- capone/tests/test_void.py | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/capone/tests/settings.py b/capone/tests/settings.py index 6e0af13..d1a5936 100644 --- a/capone/tests/settings.py +++ b/capone/tests/settings.py @@ -44,3 +44,5 @@ 'django.contrib.messages.middleware.MessageMiddleware', ) SESSION_ENGINE = 'django.contrib.sessions.backends.db' + +USE_TZ = True diff --git a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py index 5f9ac35..33dfd7d 100644 --- a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py +++ b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py @@ -1,8 +1,8 @@ -from datetime import datetime from datetime import timedelta from decimal import Decimal from django.test import TestCase +from django.utils import timezone from capone.api.actions import credit from capone.api.actions import debit @@ -22,8 +22,8 @@ def test_transaction_fields(self): """ Test filtering by `posted_timestamp`, `notes`, `type`, and `user`. """ - time = datetime.now() - wrong_time = datetime.now() - timedelta(days=1) + time = timezone.now() + wrong_time = timezone.now() - timedelta(days=1) user1 = UserFactory() user2 = UserFactory() credit_card_transaction = CreditCardTransactionFactory() diff --git a/capone/tests/test_create_transaction.py b/capone/tests/test_create_transaction.py index 538f177..e0983e9 100644 --- a/capone/tests/test_create_transaction.py +++ b/capone/tests/test_create_transaction.py @@ -1,8 +1,8 @@ -from datetime import datetime from decimal import Decimal as D from unittest import mock from django.test import TestCase +from django.utils import timezone from capone.exceptions import ExistingLedgerEntriesException from capone.exceptions import NoLedgerEntriesException @@ -153,7 +153,7 @@ def test_using_ledgers_for_reconciliation(self): ) def test_setting_posted_timestamp(self): - POSTED_DATETIME = datetime(2016, 2, 7, 11, 59) + POSTED_DATETIME = timezone.now() order = OrderFactory(amount=self.AMOUNT) txn_recognize = create_transaction( diff --git a/capone/tests/test_factories.py b/capone/tests/test_factories.py index 47e7362..a73bcd3 100644 --- a/capone/tests/test_factories.py +++ b/capone/tests/test_factories.py @@ -1,7 +1,7 @@ -from datetime import datetime from decimal import Decimal from django.test import TestCase +from django.utils import timezone from capone.api.actions import credit from capone.api.actions import debit @@ -74,7 +74,7 @@ def test_custom_fields(self): """ Test setting fields `posted_timestamp`, `notes`, `type`, and `user`. """ - time = datetime.now() + time = timezone.now() FIELDS_TO_VALUES = [ ('posted_timestamp', time), ('notes', 'booga'), diff --git a/capone/tests/test_transaction_model.py b/capone/tests/test_transaction_model.py index 3d41ba7..e3df56b 100644 --- a/capone/tests/test_transaction_model.py +++ b/capone/tests/test_transaction_model.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -from datetime import datetime from decimal import Decimal from django.test import TestCase +from django.utils import timezone from capone.api.actions import create_transaction from capone.api.actions import credit @@ -27,7 +27,7 @@ class TransactionBase(TestCase): def setUp(self): self.user1 = UserFactory() self.user2 = UserFactory() - self.posted_timestamp = datetime.now() + self.posted_timestamp = timezone.now() class TestStrMethods(TestCase): @@ -102,7 +102,7 @@ class TestSettingExplicitTimestampField(TransactionBase): def test_setting_explicit_timestamp_field(self): transaction = TransactionFactory() old_posted_timestamp = transaction.posted_timestamp - transaction.posted_timestamp = datetime.now() + transaction.posted_timestamp = timezone.now() transaction.save() self.assertNotEqual( old_posted_timestamp, diff --git a/capone/tests/test_void.py b/capone/tests/test_void.py index 60d3802..e27e194 100644 --- a/capone/tests/test_void.py +++ b/capone/tests/test_void.py @@ -1,7 +1,7 @@ -from datetime import datetime from decimal import Decimal as D from django.test import TestCase +from django.utils import timezone from capone.api.actions import create_transaction from capone.api.actions import credit @@ -182,7 +182,7 @@ def test_given_timestamp(self): LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), ]) - now = datetime.now() + now = timezone.now() void_txn = void_transaction( charge_txn, self.creation_user, posted_timestamp=now) From 366139f193127b92bb0051f94def10fe8428da25 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:08:59 -0500 Subject: [PATCH 12/30] Work around bug in psycopg2==2.9 --- tox.ini | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tox.ini b/tox.ini index 700dc10..b032e52 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,42 @@ deps = 3.2: Django>=3.2,<4 4.2: Django>=3.2,<5 +[testenv:py36-1.11] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py36-2.2] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py38-1.11] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py38-2.2] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py39-1.11] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py39-2.2] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + [testenv:flake8] commands = {envbindir}/flake8 -v capone From c8de8cecadfac3a1f4177d7246c5d19d27761254 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:11:26 -0500 Subject: [PATCH 13/30] Bump Postgres for later Django versions --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 492080b..3e641c5 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -11,7 +11,7 @@ jobs: services: postgres: - image: postgres:9.6 + image: postgres:12 env: POSTGRES_USER: django POSTGRES_PASSWORD: django From 0b07f61fa115993be2c4e7f61da75bd4eb67d615 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:14:06 -0500 Subject: [PATCH 14/30] Silence expected runtime warnings --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index b032e52..cd4118e 100644 --- a/tox.ini +++ b/tox.ini @@ -66,6 +66,8 @@ commands = {envbindir}/flake8 -v capone [pytest] DJANGO_SETTINGS_MODULE = capone.tests.settings +filterwarnings = + ignore: DateTimeField .* received a naive datetime:RuntimeWarning:: [flake8] ignore = W503 From fd7e9c3bfec4093bd11a54e6e23f093d97b8b28a Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:23:50 -0500 Subject: [PATCH 15/30] Only enable running versions for now --- .python-version | 1 - tox.ini | 14 +------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.python-version b/.python-version index 807c3e5..a168fc7 100644 --- a/.python-version +++ b/.python-version @@ -1,3 +1,2 @@ 3.6 3.8 -3.9 diff --git a/tox.ini b/tox.ini index cd4118e..3748d21 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] install_command = pip install {opts} {packages} -envlist = {py36,py38,py39}-{1.11,2.2,3.2,4.2}, flake8 +envlist = {py36,py38}-{1.11,2.2,3.2,4.2}, flake8 [testenv] passenv = LANG POSTGRES_HOST POSTGRES_DB POSTGRES_PASSWORD POSTGRES_PORT POSTGRES_USER @@ -49,18 +49,6 @@ deps = Django>=1.11,<2 psycopg2>=2.6.2,<2.9 -[testenv:py39-1.11] -deps = - -r{toxinidir}/requirements-dev.txt - Django>=1.11,<2 - psycopg2>=2.6.2,<2.9 - -[testenv:py39-2.2] -deps = - -r{toxinidir}/requirements-dev.txt - Django>=1.11,<2 - psycopg2>=2.6.2,<2.9 - [testenv:flake8] commands = {envbindir}/flake8 -v capone From 3eb8ad84b28b94524c09fd6d382b5763585082f2 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:31:46 -0500 Subject: [PATCH 16/30] Convert test suite to Pytest-style --- ...on_in_ledgers_for_amounts_with_evidence.py | 275 ++++----- capone/tests/test_create_transaction.py | 527 +++++++++--------- capone/tests/test_factories.py | 139 ++--- .../tests/test_filter_by_related_objects.py | 339 ++++++----- capone/tests/test_ledger_balances.py | 419 ++++++++------ capone/tests/test_transaction_model.py | 206 +++---- capone/tests/test_void.py | 390 +++++++------ 7 files changed, 1246 insertions(+), 1049 deletions(-) diff --git a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py index 33dfd7d..4295f87 100644 --- a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py +++ b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py @@ -1,7 +1,7 @@ from datetime import timedelta from decimal import Decimal -from django.test import TestCase +import pytest from django.utils import timezone from capone.api.actions import credit @@ -17,131 +17,68 @@ from capone.tests.factories import UserFactory -class TestAssertTransactionInLedgersForAmountsWithEvidence(TestCase): - def test_transaction_fields(self): - """ - Test filtering by `posted_timestamp`, `notes`, `type`, and `user`. - """ - time = timezone.now() - wrong_time = timezone.now() - timedelta(days=1) - user1 = UserFactory() - user2 = UserFactory() - credit_card_transaction = CreditCardTransactionFactory() - ttype1 = TransactionTypeFactory(name='1') - ttype2 = TransactionTypeFactory(name='2') - - FIELDS_TO_VALUES = [ - ('posted_timestamp', time, wrong_time), - ('notes', 'foo', 'bar'), - ('type', ttype1, ttype2), - ('user', user1, user2), - ] - - for field_name, right_value, wrong_value in FIELDS_TO_VALUES: - TransactionFactory( - evidence=[credit_card_transaction], - **{field_name: right_value}) - ledger = Ledger.objects.last() - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(Decimal('100'))), - (ledger.name, debit(Decimal('100'))), - ], - evidence=[credit_card_transaction], - **{field_name: right_value} - ) - - def test_no_matches(self): - """ - No matching transaction raises DoesNotExist. - """ - TransactionFactory() - credit_card_transaction = CreditCardTransactionFactory() +def test_transaction_fields(db): + """ + Test filtering by `posted_timestamp`, `notes`, `type`, and `user`. + """ + time = timezone.now() + wrong_time = timezone.now() - timedelta(days=1) + user1 = UserFactory() + user2 = UserFactory() + credit_card_transaction = CreditCardTransactionFactory() + ttype1 = TransactionTypeFactory(name='1') + ttype2 = TransactionTypeFactory(name='2') + + FIELDS_TO_VALUES = [ + ('posted_timestamp', time, wrong_time), + ('notes', 'foo', 'bar'), + ('type', ttype1, ttype2), + ('user', user1, user2), + ] + + for field_name, right_value, wrong_value in FIELDS_TO_VALUES: + TransactionFactory( + evidence=[credit_card_transaction], + **{field_name: right_value}) ledger = Ledger.objects.last() + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(Decimal('100'))), + (ledger.name, debit(Decimal('100'))), + ], + evidence=[credit_card_transaction], + **{field_name: right_value} + ) - self.assertTrue(Transaction.objects.exists()) - - with self.assertRaises(Transaction.DoesNotExist): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(Decimal('100'))), - (ledger.name, debit(Decimal('100'))), - ], - evidence=[credit_card_transaction], - ) - - def test_multiple_matches(self): - """ - Multiple matching transactions raises MultipleObjectsReturned. - """ - credit_card_transaction = CreditCardTransactionFactory() - amount = Decimal('100') - ledger = LedgerFactory() - for _ in range(2): - TransactionFactory( - UserFactory(), - ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=ledger), - LedgerEntry(amount=credit(amount), ledger=ledger), - ], - evidence=[credit_card_transaction], - ) - - self.assertEqual(Transaction.objects.count(), 2) - - with self.assertRaises(Transaction.MultipleObjectsReturned): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(amount)), - (ledger.name, debit(amount)), - ], - evidence=[credit_card_transaction], - ) - - def test_mismatch_on_ledger_entries(self): - """ - An otherwise matching Trans. will fail if its LedgerEntries mismatch. - """ - credit_card_transaction = CreditCardTransactionFactory() - amount = Decimal('100') - ledger = LedgerFactory() - evidence = [credit_card_transaction] - TransactionFactory( - UserFactory(), - ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=ledger), - LedgerEntry(amount=credit(amount), ledger=ledger), +def test_no_matches(db): + """ + No matching transaction raises DoesNotExist. + """ + TransactionFactory() + credit_card_transaction = CreditCardTransactionFactory() + ledger = Ledger.objects.last() + + assert Transaction.objects.exists() + + with pytest.raises(Transaction.DoesNotExist): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(Decimal('100'))), + (ledger.name, debit(Decimal('100'))), ], - evidence=evidence, + evidence=[credit_card_transaction], ) - with self.assertRaises(Transaction.DoesNotExist): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name + 'foo', credit(amount)), - (ledger.name + 'foo', debit(amount)), - ], - evidence=evidence, - ) - - with self.assertRaises(AssertionError): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(amount + Decimal('1'))), - (ledger.name, debit(amount + Decimal('1'))), - ], - evidence=evidence, - ) - - def test_mismatch_on_evidence(self): - """ - An otherwise matching Trans. will fail if its evidence is different. - """ - credit_card_transaction = CreditCardTransactionFactory() - amount = Decimal('100') - ledger = LedgerFactory() +def test_multiple_matches(db): + """ + Multiple matching transactions raises MultipleObjectsReturned. + """ + credit_card_transaction = CreditCardTransactionFactory() + amount = Decimal('100') + ledger = LedgerFactory() + for _ in range(2): TransactionFactory( UserFactory(), ledger_entries=[ @@ -151,20 +88,86 @@ def test_mismatch_on_evidence(self): evidence=[credit_card_transaction], ) - ledger_amount_pairs = [ - (ledger.name, credit(amount)), - (ledger.name, debit(amount)), - ] - - with self.assertRaises(Transaction.DoesNotExist): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=ledger_amount_pairs, - evidence=[ - credit_card_transaction, CreditCardTransactionFactory()], - ) - - with self.assertRaises(Transaction.DoesNotExist): - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=ledger_amount_pairs, - evidence=[], - ) + assert Transaction.objects.count() == 2 + + with pytest.raises(Transaction.MultipleObjectsReturned): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(amount)), + (ledger.name, debit(amount)), + ], + evidence=[credit_card_transaction], + ) + + +def test_mismatch_on_ledger_entries(db): + """ + An otherwise matching Trans. will fail if its LedgerEntries mismatch. + """ + credit_card_transaction = CreditCardTransactionFactory() + amount = Decimal('100') + ledger = LedgerFactory() + evidence = [credit_card_transaction] + + TransactionFactory( + UserFactory(), + ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ledger), + LedgerEntry(amount=credit(amount), ledger=ledger), + ], + evidence=evidence, + ) + + with pytest.raises(Transaction.DoesNotExist): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name + 'foo', credit(amount)), + (ledger.name + 'foo', debit(amount)), + ], + evidence=evidence, + ) + + with pytest.raises(AssertionError): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(amount + Decimal('1'))), + (ledger.name, debit(amount + Decimal('1'))), + ], + evidence=evidence, + ) + + +def test_mismatch_on_evidence(db): + """ + An otherwise matching Trans. will fail if its evidence is different. + """ + credit_card_transaction = CreditCardTransactionFactory() + amount = Decimal('100') + ledger = LedgerFactory() + + TransactionFactory( + UserFactory(), + ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ledger), + LedgerEntry(amount=credit(amount), ledger=ledger), + ], + evidence=[credit_card_transaction], + ) + + ledger_amount_pairs = [ + (ledger.name, credit(amount)), + (ledger.name, debit(amount)), + ] + + with pytest.raises(Transaction.DoesNotExist): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=ledger_amount_pairs, + evidence=[ + credit_card_transaction, CreditCardTransactionFactory()], + ) + + with pytest.raises(Transaction.DoesNotExist): + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=ledger_amount_pairs, + evidence=[], + ) diff --git a/capone/tests/test_create_transaction.py b/capone/tests/test_create_transaction.py index e0983e9..d9281b6 100644 --- a/capone/tests/test_create_transaction.py +++ b/capone/tests/test_create_transaction.py @@ -1,7 +1,6 @@ from decimal import Decimal as D -from unittest import mock -from django.test import TestCase +import pytest from django.utils import timezone from capone.exceptions import ExistingLedgerEntriesException @@ -21,277 +20,299 @@ from capone.tests.factories import UserFactory -RECONCILIATION_TYPE_NAME = 'Recon' +AMOUNT = D(100) -class TestCreateTransaction(TestCase): - def setUp(self): - self.AMOUNT = D(100) - self.user = UserFactory() +@pytest.fixture +def create_objects(db): + user = UserFactory() + accounts_receivable = LedgerFactory(name='Accounts Receivable') + cash_unrecon = LedgerFactory(name='Cash (unreconciled)') + cash_recon = LedgerFactory(name='Cash (reconciled)') + revenue = LedgerFactory(name='Revenue', increased_by_debits=False) + recon_ttype = TransactionTypeFactory(name='Recon') + return ( + user, + accounts_receivable, + cash_unrecon, + cash_recon, + revenue, + recon_ttype, + ) - self.accounts_receivable = LedgerFactory(name='Accounts Receivable') - self.cash_unrecon = LedgerFactory(name='Cash (unreconciled)') - self.cash_recon = LedgerFactory(name='Cash (reconciled)') - self.revenue = LedgerFactory(name='Revenue', increased_by_debits=False) - self.recon_ttype = TransactionTypeFactory( - name=RECONCILIATION_TYPE_NAME) - def test_using_ledgers_for_reconciliation(self): - """ - Test ledger behavior with a revenue reconciliation worked example. - - This test creates an Order and a CreditCardTransaction and, using the - four Ledgers created in setUp, it makes all of the ledger entries that - an Order and Transaction would be expected to have. There are three, - specifically: Revenue Recognition (credit: Revenue, debit:A/R), recording - incoming cash (credit: A/R, debit: Cash (unreconciled)) and Reconciliation - (credit: Cash (reconciled), debit: Cash (unreconciled)). - - In table form: - - Event | Accounts Receivable (unreconciled) | Revenue | Cash (unreconciled) | Cash (reconciled) | Evidence Models - ----------------------- | ---------------------------------- | ------- | ------------------- | ----------------- | -------------------------------------------------------------- - Test is complete | -$500 | +$500 | | | `Order` - Patient pays | +$500 | | -$500 | | `CreditCardTransaction` - Payments are reconciled | | | +$500 | -$500 | both `Order` and `CreditCardTransaction` - """ # noqa: E501 - order = OrderFactory() - credit_card_transaction = CreditCardTransactionFactory() - - # Assert that this Order looks "unrecognized". - self.assertEqual( - get_balances_for_object(order), - {}, - ) - - # Add an entry debiting AR and crediting Revenue: this entry should - # reference the Order. - create_transaction( - self.user, - evidence=[order], +def test_using_ledgers_for_reconciliation(create_objects): + """ + Test ledger behavior with a revenue reconciliation worked example. + + This test creates an Order and a CreditCardTransaction and, using the + four Ledgers created in setUp, it makes all of the ledger entries that + an Order and Transaction would be expected to have. There are three, + specifically: Revenue Recognition (credit: Revenue, debit:A/R), recording + incoming cash (credit: A/R, debit: Cash (unreconciled)) and Reconciliation + (credit: Cash (reconciled), debit: Cash (unreconciled)). + + In table form: + + Event | Accounts Receivable (unreconciled) | Revenue | Cash (unreconciled) | Cash (reconciled) | Evidence Models + ----------------------- | ---------------------------------- | ------- | ------------------- | ----------------- | -------------------------------------------------------------- + Test is complete | -$500 | +$500 | | | `Order` + Patient pays | +$500 | | -$500 | | `CreditCardTransaction` + Payments are reconciled | | | +$500 | -$500 | both `Order` and `CreditCardTransaction` + """ # noqa: E501 + ( + user, + accounts_receivable, + cash_unrecon, + cash_recon, + revenue, + recon_ttype, + ) = create_objects + order = OrderFactory() + credit_card_transaction = CreditCardTransactionFactory() + + # Assert that this Order looks "unrecognized". + assert get_balances_for_object(order) == {} + + # Add an entry debiting AR and crediting Revenue: this entry should + # reference the Order. + create_transaction( + user, + evidence=[order], + ledger_entries=[ + LedgerEntry( + ledger=revenue, + amount=credit(AMOUNT)), + LedgerEntry( + ledger=accounts_receivable, + amount=debit(AMOUNT)), + ], + ) + + # Assert that the correct entries were created. + assert LedgerEntry.objects.count() == 2 + assert Transaction.objects.count() == 1 + + # Assert that this Order looks "recognized". + assert get_balances_for_object(order) == { + revenue: -AMOUNT, + accounts_receivable: AMOUNT, + } + + # Add an entry crediting "A/R" and debiting "Cash (unreconciled)": this + # entry should reference the CreditCardTransaction. + create_transaction( + user, + evidence=[credit_card_transaction], + ledger_entries=[ + LedgerEntry( + ledger=accounts_receivable, + amount=credit(AMOUNT)), + LedgerEntry( + ledger=cash_unrecon, + amount=debit(AMOUNT)) + ], + ) + + # Assert that the correct entries were created + assert LedgerEntry.objects.count() == 4 + assert Transaction.objects.count() == 2 + + # Assert the CreditCardTransaction is in "Cash (unreconciled)". + assert get_balances_for_object(credit_card_transaction) == { + accounts_receivable: -AMOUNT, + cash_unrecon: AMOUNT, + } + + # Add an entry crediting "Cash (Unreconciled)" and debiting "Cash + # (Reconciled)": this entry should reference both an Order and + # a CreditCardTransaction. + create_transaction( + user, + evidence=[order, credit_card_transaction], + ledger_entries=[ + LedgerEntry( + ledger=cash_unrecon, + amount=credit(AMOUNT)), + LedgerEntry( + ledger=cash_recon, + amount=debit(AMOUNT)) + ], + type=recon_ttype, + ) + + # Assert that the correct entries were created. + assert LedgerEntry.objects.count() == 6 + assert Transaction.objects.count() == 3 + + # Assert that revenue is recognized and reconciled. + assert get_balances_for_object(order) == { + accounts_receivable: AMOUNT, + cash_unrecon: -AMOUNT, + cash_recon: AMOUNT, + revenue: -AMOUNT, + } + + +def test_setting_posted_timestamp(create_objects): + ( + user, + accounts_receivable, + cash_unrecon, + cash_recon, + revenue, + recon_ttype, + ) = create_objects + POSTED_DATETIME = timezone.now() + order = OrderFactory(amount=AMOUNT) + + txn_recognize = create_transaction( + user, + evidence=[order], + ledger_entries=[ + LedgerEntry( + ledger=revenue, + amount=credit(AMOUNT)), + LedgerEntry( + ledger=accounts_receivable, + amount=debit(AMOUNT)), + ], + posted_timestamp=POSTED_DATETIME, + ) + + assert txn_recognize.posted_timestamp == POSTED_DATETIME + + +def test_debits_not_equal_to_credits(create_objects): + ( + user, + accounts_receivable, + cash_unrecon, + cash_recon, + revenue, + recon_ttype, + ) = create_objects + with pytest.raises(TransactionBalanceException): + validate_transaction( + user, ledger_entries=[ LedgerEntry( - ledger=self.revenue, - amount=credit(self.AMOUNT)), + ledger=revenue, + amount=credit(AMOUNT)), LedgerEntry( - ledger=self.accounts_receivable, - amount=debit(self.AMOUNT)), + ledger=accounts_receivable, + amount=debit(AMOUNT + 2)), ], ) - # Assert that the correct entries were created. - self.assertEqual(LedgerEntry.objects.count(), 2) - self.assertEqual(Transaction.objects.count(), 1) - - # Assert that this Order looks "recognized". - self.assertEqual( - get_balances_for_object(order), - { - self.revenue: -self.AMOUNT, - self.accounts_receivable: self.AMOUNT, - }, - ) - # Add an entry crediting "A/R" and debiting "Cash (unreconciled)": this - # entry should reference the CreditCardTransaction. +def test_no_ledger_entries(create_objects): + ( + user, + accounts_receivable, + cash_unrecon, + cash_recon, + revenue, + recon_ttype, + ) = create_objects + with pytest.raises(NoLedgerEntriesException): + validate_transaction(user) + + +def test_with_existing_ledger_entry(db): + amount = D(100) + user = UserFactory() + + accounts_receivable = LedgerFactory(name='Accounts Receivable') + + existing_transaction = create_transaction( + user, + ledger_entries=[ + LedgerEntry( + ledger=accounts_receivable, + amount=credit(amount)), + LedgerEntry( + ledger=accounts_receivable, + amount=debit(amount)), + ], + ) + + with pytest.raises(ExistingLedgerEntriesException): create_transaction( - self.user, - evidence=[credit_card_transaction], - ledger_entries=[ - LedgerEntry( - ledger=self.accounts_receivable, - amount=credit(self.AMOUNT)), - LedgerEntry( - ledger=self.cash_unrecon, - amount=debit(self.AMOUNT)) - ], + user, + ledger_entries=list(existing_transaction.entries.all()), ) - # Assert that the correct entries were created - self.assertEqual(LedgerEntry.objects.count(), 4) - self.assertEqual(Transaction.objects.count(), 2) - - # Assert the CreditCardTransaction is in "Cash (unreconciled)". - self.assertEqual( - get_balances_for_object(credit_card_transaction), - { - self.accounts_receivable: -self.AMOUNT, - self.cash_unrecon: self.AMOUNT, - }, - ) - # Add an entry crediting "Cash (Unreconciled)" and debiting "Cash - # (Reconciled)": this entry should reference both an Order and - # a CreditCardTransaction. - create_transaction( - self.user, - evidence=[order, credit_card_transaction], - ledger_entries=[ - LedgerEntry( - ledger=self.cash_unrecon, - amount=credit(self.AMOUNT)), - LedgerEntry( - ledger=self.cash_recon, - amount=debit(self.AMOUNT)) - ], - type=self.recon_ttype, - ) +def test_credit_and_debit_helper_functions(settings): + """ + Test that `credit` and `debit` return the correctly signed amounts. + """ + settings.DEBITS_ARE_NEGATIVE = True + assert credit(AMOUNT) > 0 + assert debit(AMOUNT) < 0 - # Assert that the correct entries were created. - self.assertEqual(LedgerEntry.objects.count(), 6) - self.assertEqual(Transaction.objects.count(), 3) - - # Assert that revenue is recognized and reconciled. - self.assertEqual( - get_balances_for_object(order), - { - self.accounts_receivable: self.AMOUNT, - self.cash_unrecon: -self.AMOUNT, - self.cash_recon: self.AMOUNT, - self.revenue: -self.AMOUNT, - }, - ) + settings.DEBITS_ARE_NEGATIVE = False + assert credit(AMOUNT) < 0 + assert debit(AMOUNT) > 0 - def test_setting_posted_timestamp(self): - POSTED_DATETIME = timezone.now() - order = OrderFactory(amount=self.AMOUNT) - txn_recognize = create_transaction( - self.user, - evidence=[order], - ledger_entries=[ - LedgerEntry( - ledger=self.revenue, - amount=credit(self.AMOUNT)), - LedgerEntry( - ledger=self.accounts_receivable, - amount=debit(self.AMOUNT)), - ], - posted_timestamp=POSTED_DATETIME, - ) +def test_validation_error(): + """ + Test that `credit` and `debit` return the correctly signed amounts. + """ + pytest.raises(ValueError, credit, -AMOUNT) + pytest.raises(ValueError, debit, -AMOUNT) - self.assertEqual(txn_recognize.posted_timestamp, POSTED_DATETIME) - - def test_debits_not_equal_to_credits(self): - with self.assertRaises(TransactionBalanceException): - validate_transaction( - self.user, - ledger_entries=[ - LedgerEntry( - ledger=self.revenue, - amount=credit(self.AMOUNT)), - LedgerEntry( - ledger=self.accounts_receivable, - amount=debit(self.AMOUNT + 2)), - ], - ) - - def test_no_ledger_entries(self): - with self.assertRaises(NoLedgerEntriesException): - validate_transaction( - self.user, - ) - - -class TestExistingLedgerEntriesException(TestCase): - def setUp(self): - self.amount = D(100) - self.user = UserFactory() - - self.accounts_receivable = LedgerFactory(name='Accounts Receivable') - self.cash = LedgerFactory(name='Cash') - - def test_with_existing_ledger_entry(self): - existing_transaction = create_transaction( - self.user, - ledger_entries=[ - LedgerEntry( - ledger=self.accounts_receivable, - amount=credit(self.amount)), - LedgerEntry( - ledger=self.accounts_receivable, - amount=debit(self.amount)), - ], - ) - with self.assertRaises(ExistingLedgerEntriesException): - create_transaction( - self.user, - ledger_entries=list(existing_transaction.entries.all()), - ) +def _create_transaction_and_compare_to_amount( + amount, comparison_amount=None): + ledger1 = LedgerFactory() + ledger2 = LedgerFactory() + transaction = create_transaction( + UserFactory(), + ledger_entries=[ + LedgerEntry( + ledger=ledger1, + amount=amount), + LedgerEntry( + ledger=ledger2, + amount=-amount), + ] + ) + entry1 = transaction.entries.get(ledger=ledger1) + entry2 = transaction.entries.get(ledger=ledger2) + if comparison_amount: + assert entry1.amount != amount + assert entry1.amount == comparison_amount + assert entry2.amount != -amount + assert -entry2.amount == comparison_amount + else: + assert entry1.amount == amount + assert entry2.amount == -amount + + +def test_precision(db): + _create_transaction_and_compare_to_amount( + D('-499.9999')) + + +def test_round_up(db): + _create_transaction_and_compare_to_amount( + D('499.99995'), D('500')) + + +def test_round_down(db): + _create_transaction_and_compare_to_amount( + D('499.99994'), D('499.9999')) + + +def test_round_up_negative(db): + _create_transaction_and_compare_to_amount( + D('-499.99994'), D('-499.9999')) -class TestCreditAndDebit(TestCase): - """ - Test that `credit` and `debit` return the correctly signed amounts. - """ - AMOUNT = D(100) - - def assertPositive(self, amount): - self.assertGreaterEqual(amount, 0) - - def assertNegative(self, amount): - self.assertLess(amount, 0) - - def test_credit_and_debit_helper_functions(self): - with mock.patch('capone.api.actions.settings') as mock_settings: - mock_settings.DEBITS_ARE_NEGATIVE = True - self.assertPositive(credit(self.AMOUNT)) - self.assertNegative(debit(self.AMOUNT)) - with mock.patch('capone.api.actions.settings') as mock_settings: - mock_settings.DEBITS_ARE_NEGATIVE = False - self.assertNegative(credit(self.AMOUNT)) - self.assertPositive(debit(self.AMOUNT)) - - def test_validation_error(self): - self.assertRaises(ValueError, credit, -self.AMOUNT) - self.assertRaises(ValueError, debit, -self.AMOUNT) - - -class TestRounding(TestCase): - def _create_transaction_and_compare_to_amount( - self, amount, comparison_amount=None): - ledger1 = LedgerFactory() - ledger2 = LedgerFactory() - transaction = create_transaction( - UserFactory(), - ledger_entries=[ - LedgerEntry( - ledger=ledger1, - amount=amount), - LedgerEntry( - ledger=ledger2, - amount=-amount), - ] - ) - entry1 = transaction.entries.get(ledger=ledger1) - entry2 = transaction.entries.get(ledger=ledger2) - if comparison_amount: - self.assertNotEqual(entry1.amount, amount) - self.assertEqual(entry1.amount, comparison_amount) - self.assertNotEqual(entry2.amount, -amount) - self.assertEqual(-entry2.amount, comparison_amount) - else: - self.assertEqual(entry1.amount, amount) - self.assertEqual(entry2.amount, -amount) - - def test_precision(self): - self._create_transaction_and_compare_to_amount( - D('-499.9999')) - - def test_round_up(self): - self._create_transaction_and_compare_to_amount( - D('499.99995'), D('500')) - - def test_round_down(self): - self._create_transaction_and_compare_to_amount( - D('499.99994'), D('499.9999')) - - def test_round_up_negative(self): - self._create_transaction_and_compare_to_amount( - D('-499.99994'), D('-499.9999')) - - def test_round_down_negative(self): - self._create_transaction_and_compare_to_amount( - D('-499.99995'), D('-500')) +def test_round_down_negative(db): + _create_transaction_and_compare_to_amount( + D('-499.99995'), D('-500')) diff --git a/capone/tests/test_factories.py b/capone/tests/test_factories.py index a73bcd3..79bb460 100644 --- a/capone/tests/test_factories.py +++ b/capone/tests/test_factories.py @@ -1,6 +1,6 @@ from decimal import Decimal -from django.test import TestCase +import pytest from django.utils import timezone from capone.api.actions import credit @@ -15,83 +15,88 @@ from capone.tests.factories import UserFactory -class TestTransactionFactory(TestCase): - """ - Test TransactionFactory. +""" +Test TransactionFactory. - We test this "factory" because it's actually a method implemented in this - app, not a Factory Boy Factory. - """ - @classmethod - def setUpTestData(cls): - cls.credit_card_transaction = CreditCardTransactionFactory() +We test this "factory" because it's actually a method implemented in this +app, not a Factory Boy Factory. +""" - def test_no_args(self): - TransactionFactory(evidence=[self.credit_card_transaction]) - ledger = Ledger.objects.last() - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(Decimal('100'))), - (ledger.name, debit(Decimal('100'))), - ], - evidence=[self.credit_card_transaction], - ) +@pytest.fixture +def credit_card_transaction(db): + return CreditCardTransactionFactory() - def test_custom_ledger_entries(self): - ledger = LedgerFactory() - amount = Decimal('500') - TransactionFactory( - evidence=[self.credit_card_transaction], - ledger_entries=[ - LedgerEntry(ledger=ledger, amount=credit(amount)), - LedgerEntry(ledger=ledger, amount=debit(amount)), - ] - ) - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(amount)), - (ledger.name, debit(amount)), - ], - evidence=[self.credit_card_transaction], - ) +def test_no_args(credit_card_transaction): + TransactionFactory(evidence=[credit_card_transaction]) - def test_custom_evidence(self): - ccx = CreditCardTransactionFactory() - TransactionFactory(evidence=[ccx]) + ledger = Ledger.objects.last() + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(Decimal('100'))), + (ledger.name, debit(Decimal('100'))), + ], + evidence=[credit_card_transaction], + ) + +def test_custom_ledger_entries(credit_card_transaction): + ledger = LedgerFactory() + amount = Decimal('500') + TransactionFactory( + evidence=[credit_card_transaction], + ledger_entries=[ + LedgerEntry(ledger=ledger, amount=credit(amount)), + LedgerEntry(ledger=ledger, amount=debit(amount)), + ] + ) + + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(amount)), + (ledger.name, debit(amount)), + ], + evidence=[credit_card_transaction], + ) + + +def test_custom_evidence(db): + ccx = CreditCardTransactionFactory() + TransactionFactory(evidence=[ccx]) + + ledger = Ledger.objects.last() + assert_transaction_in_ledgers_for_amounts_with_evidence( + ledger_amount_pairs=[ + (ledger.name, credit(Decimal('100'))), + (ledger.name, debit(Decimal('100'))), + ], + evidence=[ccx], + ) + + +def test_custom_fields(credit_card_transaction): + """ + Test setting fields `posted_timestamp`, `notes`, `type`, and `user`. + """ + time = timezone.now() + FIELDS_TO_VALUES = [ + ('posted_timestamp', time), + ('notes', 'booga'), + ('type', TransactionTypeFactory()), + ('user', UserFactory()), + ] + + for field_name, value in FIELDS_TO_VALUES: + TransactionFactory( + evidence=[credit_card_transaction], + **{field_name: value}) ledger = Ledger.objects.last() assert_transaction_in_ledgers_for_amounts_with_evidence( ledger_amount_pairs=[ (ledger.name, credit(Decimal('100'))), (ledger.name, debit(Decimal('100'))), ], - evidence=[ccx], + evidence=[credit_card_transaction], + **{field_name: value} ) - - def test_custom_fields(self): - """ - Test setting fields `posted_timestamp`, `notes`, `type`, and `user`. - """ - time = timezone.now() - FIELDS_TO_VALUES = [ - ('posted_timestamp', time), - ('notes', 'booga'), - ('type', TransactionTypeFactory()), - ('user', UserFactory()), - ] - - for field_name, value in FIELDS_TO_VALUES: - TransactionFactory( - evidence=[self.credit_card_transaction], - **{field_name: value}) - ledger = Ledger.objects.last() - assert_transaction_in_ledgers_for_amounts_with_evidence( - ledger_amount_pairs=[ - (ledger.name, credit(Decimal('100'))), - (ledger.name, debit(Decimal('100'))), - ], - evidence=[self.credit_card_transaction], - **{field_name: value} - ) diff --git a/capone/tests/test_filter_by_related_objects.py b/capone/tests/test_filter_by_related_objects.py index 8ca9ea4..f9451d2 100644 --- a/capone/tests/test_filter_by_related_objects.py +++ b/capone/tests/test_filter_by_related_objects.py @@ -1,7 +1,6 @@ from decimal import Decimal as D -from django.test import TestCase -from parameterized import parameterized +import pytest from capone.api.actions import create_transaction from capone.api.actions import credit @@ -14,167 +13,205 @@ from capone.tests.factories import UserFactory -class TestFilterByRelatedObjects(TestCase): - """ - Test Transaction.objects.filter_by_related_objects. - """ - AMOUNT = D('100') +""" +Test Transaction.objects.filter_by_related_objects. +""" +AMOUNT = D('100') + + +@pytest.fixture +def create_transactions(db): + create_user = UserFactory() + ledger = LedgerFactory() - @classmethod - def _create_transaction_with_evidence(cls, evidence): + def _create_transaction_with_evidence(evidence): return create_transaction( - cls.create_user, + create_user, evidence=evidence, ledger_entries=[ LedgerEntry( - ledger=cls.ledger, - amount=credit(cls.AMOUNT)), + ledger=ledger, + amount=credit(AMOUNT)), LedgerEntry( - ledger=cls.ledger, - amount=debit(cls.AMOUNT)), + ledger=ledger, + amount=debit(AMOUNT)), ] ) - @classmethod - def setUpTestData(cls): - cls.create_user = UserFactory() - - cls.order_1 = OrderFactory() - cls.order_2 = OrderFactory() - - cls.ledger = LedgerFactory() + order_1 = OrderFactory() + order_2 = OrderFactory() + + transaction_with_both_orders = ( + _create_transaction_with_evidence([ + order_1, + order_2, + ]) + ) + transaction_with_only_order_1 = ( + _create_transaction_with_evidence([ + order_1, + ]) + ) + transaction_with_only_order_2 = ( + _create_transaction_with_evidence([ + order_1, + ]) + ) + transaction_with_neither_order = ( + _create_transaction_with_evidence([ + OrderFactory(), + ]) + ) + + transaction_with_three_orders = ( + _create_transaction_with_evidence([ + order_1, + order_2, + OrderFactory(), + ]) + ) + + return ( + order_1, + order_2, + transaction_with_both_orders, + transaction_with_only_order_1, + transaction_with_only_order_2, + transaction_with_neither_order, + transaction_with_three_orders, + ledger, + ) + + +@pytest.mark.parametrize("match_type,queryset_function_name", [ + (MatchType.ANY, 'all'), + (MatchType.ALL, 'all'), + (MatchType.NONE, 'all'), + (MatchType.EXACT, 'none'), +]) +def test_filter_with_no_evidence( + match_type, queryset_function_name, create_transactions, db, +): + """ + Method returns correct Transactions with no evidence given. + """ + result_queryset = getattr( + Transaction.objects, queryset_function_name)().values_list('id') + assert set(result_queryset) == set( + Transaction.objects.filter_by_related_objects( + [], match_type=match_type).values_list('id') + ) + + +@pytest.mark.parametrize("match_type,results", [ + (MatchType.ANY, [True, True, True, False, True]), + (MatchType.ALL, [True, False, False, False, True]), + (MatchType.NONE, [False, False, False, True, False]), + (MatchType.EXACT, [True, False, False, False, False]), +]) +def test_filters(match_type, results, create_transactions, db): + """ + Method returns correct Transactions with various evidence given. - cls.transaction_with_both_orders = ( - cls._create_transaction_with_evidence([ - cls.order_1, - cls.order_2, - ]) - ) - cls.transaction_with_only_order_1 = ( - cls._create_transaction_with_evidence([ - cls.order_1, - ]) - ) - cls.transaction_with_only_order_2 = ( - cls._create_transaction_with_evidence([ - cls.order_1, - ]) - ) - cls.transaction_with_neither_order = ( - cls._create_transaction_with_evidence([ - OrderFactory(), - ]) - ) + This test uses the differing groups of transactions from + `setUpTestData` to test that different `MatchTypes` give the right + results. Note that the list of booleans in the `parameterized.expand` + decorator maps to the querysets in `query_list`. + """ + ( + order_1, + order_2, + transaction_with_both_orders, + transaction_with_only_order_1, + transaction_with_only_order_2, + transaction_with_neither_order, + transaction_with_three_orders, + ledger, + ) = create_transactions + + query_list = [ + transaction_with_both_orders, + transaction_with_only_order_1, + transaction_with_only_order_2, + transaction_with_neither_order, + transaction_with_three_orders, + ] + + for query, query_should_be_in_result in zip(query_list, results): + if query_should_be_in_result: + assert query in Transaction.objects.filter_by_related_objects( + [order_1, order_2], + match_type=match_type + ) + else: + assert query not in Transaction.objects.filter_by_related_objects( + [order_1, order_2], match_type=match_type + ) + + +@pytest.mark.parametrize("match_type,query_counts", [ + (MatchType.ANY, 1), + (MatchType.ALL, 1), + (MatchType.NONE, 1), + (MatchType.EXACT, 4), +]) +def test_query_counts( + match_type, + query_counts, + django_assert_num_queries, + create_transactions, + db, +): + """ + `filter_by_related_objects` should use a constant number of queries. + """ + (order_1, order_2, _, _, _, _, _, _) = create_transactions + with django_assert_num_queries(query_counts): + list(Transaction.objects.filter_by_related_objects( + [order_1], + match_type=match_type + )) - cls.transaction_with_three_orders = ( - cls._create_transaction_with_evidence([ - cls.order_1, - cls.order_2, - OrderFactory(), - ]) - ) + with django_assert_num_queries(query_counts): + list(Transaction.objects.filter_by_related_objects( + [order_1, order_2], + match_type=match_type + )) - @parameterized.expand([ - (MatchType.ANY, 'all'), - (MatchType.ALL, 'all'), - (MatchType.NONE, 'all'), - (MatchType.EXACT, 'none'), - ]) - def test_filter_with_no_evidence(self, match_type, queryset_function_name): - """ - Method returns correct Transactions with no evidence given. - """ - result_queryset = getattr( - Transaction.objects, queryset_function_name)().values_list('id') - self.assertEqual( - set(result_queryset), - set(Transaction.objects.filter_by_related_objects( - [], match_type=match_type).values_list('id')) - ) - @parameterized.expand([ - (MatchType.ANY, [True, True, True, False, True]), - (MatchType.ALL, [True, False, False, False, True]), - (MatchType.NONE, [False, False, False, True, False]), - (MatchType.EXACT, [True, False, False, False, False]), - ]) - def test_filters(self, match_type, results): - """ - Method returns correct Transactions with various evidence given. - - This test uses the differing groups of transactions from - `setUpTestData` to test that different `MatchTypes` give the right - results. Note that the list of booleans in the `parameterized.expand` - decorator maps to the querysets in `query_list`. - """ - query_list = [ - self.transaction_with_both_orders, - self.transaction_with_only_order_1, - self.transaction_with_only_order_2, - self.transaction_with_neither_order, - self.transaction_with_three_orders, - ] - - for query, query_should_be_in_result in zip(query_list, results): - if query_should_be_in_result: - self.assertIn( - query, - Transaction.objects.filter_by_related_objects( - [self.order_1, self.order_2], - match_type=match_type - ) - ) - else: - self.assertNotIn( - query, - Transaction.objects.filter_by_related_objects([ - self.order_1, self.order_2, - ], match_type=match_type) - ) - - @parameterized.expand([ - (MatchType.ANY, 1), - (MatchType.ALL, 1), - (MatchType.NONE, 1), - (MatchType.EXACT, 4), - ]) - def test_query_counts(self, match_type, query_counts): - """ - `filter_by_related_objects` should use a constant number of queries. - """ - with self.assertNumQueries(query_counts): - list(Transaction.objects.filter_by_related_objects( - [self.order_1], - match_type=match_type - )) +def test_invalid_match_type(): + """ + Invalid MatchTypes are not allowed. + """ + with pytest.raises(ValueError): + Transaction.objects.filter_by_related_objects(match_type='foo') - with self.assertNumQueries(query_counts): - list(Transaction.objects.filter_by_related_objects( - [self.order_1, self.order_2], - match_type=match_type - )) - - def test_invalid_match_type(self): - """ - Invalid MatchTypes are not allowed. - """ - with self.assertRaises(ValueError): - Transaction.objects.filter_by_related_objects(match_type='foo') - - def test_chaining_filter_to_existing_queryset(self): - """ - `filter_by_related_objects` can be used like any other queryset filter. - """ - self.assertEqual(Transaction.objects.count(), 5) - - self.assertEqual( - Transaction.objects.filter_by_related_objects( - [self.order_1]).count(), 4) - - transactions_restricted_by_ledger = ( - Transaction.objects.filter(ledgers__in=[self.ledger]) - ) - self.assertEqual( - transactions_restricted_by_ledger.filter_by_related_objects( - [self.order_1]).distinct().count(), 4) +def test_chaining_filter_to_existing_queryset(create_transactions, db): + """ + `filter_by_related_objects` can be used like any other queryset filter. + """ + ( + order_1, + order_2, + transaction_with_both_orders, + transaction_with_only_order_1, + transaction_with_only_order_2, + transaction_with_neither_order, + transaction_with_three_orders, + ledger, + ) = create_transactions + + assert Transaction.objects.count() == 5 + + assert Transaction.objects.filter_by_related_objects( + [order_1], + ).count() == 4 + + transactions_restricted_by_ledger = ( + Transaction.objects.filter(ledgers__in=[ledger]) + ) + + assert transactions_restricted_by_ledger.filter_by_related_objects( + [order_1] + ).distinct().count() == 4 diff --git a/capone/tests/test_ledger_balances.py b/capone/tests/test_ledger_balances.py index 1a5a7a5..1cbd752 100644 --- a/capone/tests/test_ledger_balances.py +++ b/capone/tests/test_ledger_balances.py @@ -1,8 +1,8 @@ from collections import defaultdict from decimal import Decimal +import pytest from django.db.models import F -from django.test import TransactionTestCase from capone.api.actions import create_transaction from capone.api.actions import credit @@ -20,193 +20,302 @@ from capone.utils import rebuild_ledger_balances -class TestLedgerBalances(TransactionTestCase): - """ - Test that `LedgerBalances` are automatically created and updated. - """ +""" +Test that `LedgerBalances` are automatically created and updated. +""" - amount = Decimal('50.00') +amount = Decimal('50.00') - def setUp(self): - self.order_1, self.order_2 = OrderFactory.create_batch(2) - self.ar_ledger = LedgerFactory(name='A/R') - self.cash_ledger = LedgerFactory(name='Cash') - self.other_ledger = LedgerFactory(name='Other') - self.user = UserFactory() - def tearDown(self): - Transaction.objects.all().delete() - ( - Ledger.objects - .filter(id__in=(self.ar_ledger.id, self.cash_ledger.id)) - .delete() - ) - self.order_1.delete() - self.order_2.delete() - self.user.delete() +@pytest.fixture +def create_objects(db): + order_1, order_2 = OrderFactory.create_batch(2) + ar_ledger = LedgerFactory(name='A/R') + cash_ledger = LedgerFactory(name='Cash') + other_ledger = LedgerFactory(name='Other') + user = UserFactory() + + yield (order_1, order_2, ar_ledger, cash_ledger, other_ledger, user) + + Transaction.objects.all().delete() + ( + Ledger.objects + .filter(id__in=(ar_ledger.id, cash_ledger.id)) + .delete() + ) + order_1.delete() + order_2.delete() + user.delete() + + +def assert_objects_have_ledger_balances(other_ledger, object_ledger_balances): + obj_to_ledger_balances = defaultdict(dict) - def assert_objects_have_ledger_balances(self, *object_ledger_balances): - obj_to_ledger_balances = defaultdict(dict) + for obj, ledger, balance in object_ledger_balances: + if balance is not None: + obj_to_ledger_balances[obj][ledger] = balance - for obj, ledger, balance in object_ledger_balances: - if balance is not None: - obj_to_ledger_balances[obj][ledger] = balance + for obj, expected_balances in obj_to_ledger_balances.items(): + actual_balances = get_balances_for_object(obj) + assert actual_balances == expected_balances + assert other_ledger not in actual_balances + assert actual_balances[other_ledger] == Decimal(0) - for obj, expected_balances in obj_to_ledger_balances.items(): - actual_balances = get_balances_for_object(obj) - self.assertEqual(actual_balances, expected_balances) - self.assertNotIn(self.other_ledger, actual_balances) - self.assertEqual(actual_balances[self.other_ledger], Decimal(0)) - def add_transaction(self, orders): +def test_no_balances(create_objects): + ( + order_1, + order_2, + ar_ledger, + cash_ledger, + other_ledger, + _, + ) = create_objects + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, None), + (order_1, cash_ledger, None), + (order_2, ar_ledger, None), + (order_2, cash_ledger, None), + ] + ) + + +def test_ledger_balance_update(create_objects): + ( + order_1, + order_2, + ar_ledger, + cash_ledger, + other_ledger, + user, + ) = create_objects + + def add_transaction(orders): return create_transaction( - self.user, + user, evidence=orders, ledger_entries=[ LedgerEntry( - ledger=self.ar_ledger, - amount=credit(self.amount)), + ledger=ar_ledger, + amount=credit(amount)), LedgerEntry( - ledger=self.cash_ledger, - amount=debit(self.amount)), + ledger=cash_ledger, + amount=debit(amount)), ], ) - def test_no_balances(self): - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, None), - (self.order_1, self.cash_ledger, None), - (self.order_2, self.ar_ledger, None), - (self.order_2, self.cash_ledger, None), - ) + add_transaction([order_1]) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount)), + (order_1, cash_ledger, debit(amount)), + (order_2, ar_ledger, None), + (order_2, cash_ledger, None), + ] + ) - def test_ledger_balance_update(self): - self.add_transaction([self.order_1]) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount)), - (self.order_1, self.cash_ledger, debit(self.amount)), - (self.order_2, self.ar_ledger, None), - (self.order_2, self.cash_ledger, None), - ) + add_transaction([order_2]) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount)), + (order_1, cash_ledger, debit(amount)), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) - self.add_transaction([self.order_2]) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount)), - (self.order_1, self.cash_ledger, debit(self.amount)), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) + add_transaction([order_1]) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 2), + (order_1, cash_ledger, debit(amount) * 2), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) - self.add_transaction([self.order_1]) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 2), - (self.order_1, self.cash_ledger, debit(self.amount) * 2), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) + transaction = add_transaction([order_1, order_2]) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 3), + (order_1, cash_ledger, debit(amount) * 3), + (order_2, ar_ledger, credit(amount) * 2), + (order_2, cash_ledger, debit(amount) * 2), + ] + ) - transaction = self.add_transaction([self.order_1, self.order_2]) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 3), - (self.order_1, self.cash_ledger, debit(self.amount) * 3), - (self.order_2, self.ar_ledger, credit(self.amount) * 2), - (self.order_2, self.cash_ledger, debit(self.amount) * 2), - ) + void_transaction(transaction, user) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 2), + (order_1, cash_ledger, debit(amount) * 2), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) - void_transaction(transaction, self.user) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 2), - (self.order_1, self.cash_ledger, debit(self.amount) * 2), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) - def test_rebuild_ledger_balance(self): - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, None), - (self.order_1, self.cash_ledger, None), - (self.order_2, self.ar_ledger, None), - (self.order_2, self.cash_ledger, None), - ) +def test_rebuild_ledger_balance(create_objects, transactional_db): + ( + order_1, + order_2, + ar_ledger, + cash_ledger, + other_ledger, + user, + ) = create_objects - self.add_transaction([self.order_1]) - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount)), - (self.order_1, self.cash_ledger, debit(self.amount)), - (self.order_2, self.ar_ledger, None), - (self.order_2, self.cash_ledger, None), + def add_transaction(orders): + return create_transaction( + user, + evidence=orders, + ledger_entries=[ + LedgerEntry( + ledger=ar_ledger, + amount=credit(amount)), + LedgerEntry( + ledger=cash_ledger, + amount=debit(amount)), + ], ) - self.add_transaction([self.order_2]) - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount)), - (self.order_1, self.cash_ledger, debit(self.amount)), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, None), + (order_1, cash_ledger, None), + (order_2, ar_ledger, None), + (order_2, cash_ledger, None), + ] + ) - self.add_transaction([self.order_1]) - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 2), - (self.order_1, self.cash_ledger, debit(self.amount) * 2), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) + add_transaction([order_1]) + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount)), + (order_1, cash_ledger, debit(amount)), + (order_2, ar_ledger, None), + (order_2, cash_ledger, None), + ] + ) - transaction = self.add_transaction([self.order_1, self.order_2]) - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 3), - (self.order_1, self.cash_ledger, debit(self.amount) * 3), - (self.order_2, self.ar_ledger, credit(self.amount) * 2), - (self.order_2, self.cash_ledger, debit(self.amount) * 2), - ) + add_transaction([order_2]) + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount)), + (order_1, cash_ledger, debit(amount)), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) + + add_transaction([order_1]) + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 2), + (order_1, cash_ledger, debit(amount) * 2), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) + + transaction = add_transaction([order_1, order_2]) + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 3), + (order_1, cash_ledger, debit(amount) * 3), + (order_2, ar_ledger, credit(amount) * 2), + (order_2, cash_ledger, debit(amount) * 2), + ] + ) + + void_transaction(transaction, user) + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 2), + (order_1, cash_ledger, debit(amount) * 2), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) + + LedgerBalance.objects.update(balance=Decimal('1.00')) + LedgerBalance.objects.first().delete() + rebuild_ledger_balances() + assert_objects_have_ledger_balances( + other_ledger, + [ + (order_1, ar_ledger, credit(amount) * 2), + (order_1, cash_ledger, debit(amount) * 2), + (order_2, ar_ledger, credit(amount)), + (order_2, cash_ledger, debit(amount)), + ] + ) - void_transaction(transaction, self.user) - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 2), - (self.order_1, self.cash_ledger, debit(self.amount) * 2), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), - ) - LedgerBalance.objects.update(balance=Decimal('1.00')) - LedgerBalance.objects.first().delete() - rebuild_ledger_balances() - self.assert_objects_have_ledger_balances( - (self.order_1, self.ar_ledger, credit(self.amount) * 2), - (self.order_1, self.cash_ledger, debit(self.amount) * 2), - (self.order_2, self.ar_ledger, credit(self.amount)), - (self.order_2, self.cash_ledger, debit(self.amount)), +def test_ledger_balances_filtering(db, create_objects): + ( + order_1, + order_2, + ar_ledger, + cash_ledger, + other_ledger, + user, + ) = create_objects + + def add_transaction(orders): + return create_transaction( + user, + evidence=orders, + ledger_entries=[ + LedgerEntry( + ledger=ar_ledger, + amount=credit(amount)), + LedgerEntry( + ledger=cash_ledger, + amount=debit(amount)), + ], ) - def test_ledger_balances_filtering(self): - Order.objects.update(amount=self.amount * 2) - - def all_cash_orders(): - return set( - Order.objects - .filter( - id__in=(self.order_1.id, self.order_2.id), - ledger_balances__ledger=self.cash_ledger, - ledger_balances__balance=F('amount'), - ) + Order.objects.update(amount=amount * 2) + + def all_cash_orders(): + return set( + Order.objects + .filter( + id__in=(order_1.id, order_2.id), + ledger_balances__ledger=cash_ledger, + ledger_balances__balance=F('amount'), ) + ) - self.assertEqual(all_cash_orders(), set()) + assert all_cash_orders() == set() - self.add_transaction([self.order_1]) - self.assertEqual(all_cash_orders(), set()) + add_transaction([order_1]) + assert all_cash_orders() == set() - self.add_transaction([self.order_1]) - self.assertEqual(all_cash_orders(), {self.order_1}) + add_transaction([order_1]) + assert all_cash_orders() == {order_1} - self.add_transaction([self.order_2]) - self.assertEqual(all_cash_orders(), {self.order_1}) + add_transaction([order_2]) + assert all_cash_orders() == {order_1} - self.add_transaction([self.order_2]) - self.assertEqual(all_cash_orders(), {self.order_1, self.order_2}) + add_transaction([order_2]) + assert all_cash_orders() == {order_1, order_2} diff --git a/capone/tests/test_transaction_model.py b/capone/tests/test_transaction_model.py index e3df56b..59b376f 100644 --- a/capone/tests/test_transaction_model.py +++ b/capone/tests/test_transaction_model.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from decimal import Decimal -from django.test import TestCase +import pytest from django.utils import timezone from capone.api.actions import create_transaction @@ -20,97 +20,76 @@ from capone.tests.factories import UserFactory -class TransactionBase(TestCase): - """ - Base class for `Transaction` model test cases. - """ - def setUp(self): - self.user1 = UserFactory() - self.user2 = UserFactory() - self.posted_timestamp = timezone.now() - - -class TestStrMethods(TestCase): +def test_unicode_methods(db): """ Test all __str__ methods. """ - def test_unicode_methods(self): - txn = TransactionFactory() - - tro = txn.related_objects.last() - self.assertEqual( - str(tro), - 'TransactionRelatedObject: CreditCardTransaction(id=%s)' % tro.related_object_id, # noqa: E501 + txn = TransactionFactory() + + tro = txn.related_objects.last() + assert str(tro) == ( + 'TransactionRelatedObject: CreditCardTransaction(id=%s)' + % tro.related_object_id + ) + + entry = txn.entries.last() + assert str(entry) == ( + "LedgerEntry: $%s in %s" % ( + entry.amount, + entry.ledger.name, ) - - entry = txn.entries.last() - self.assertEqual( - str(entry), - "LedgerEntry: $%s in %s" % ( - entry.amount, - entry.ledger.name, - ) - ) - - ledger = LedgerFactory(name='foo') - self.assertEqual(str(ledger), "Ledger foo") - ledger = LedgerFactory(name='föo') - self.assertTrue(str(ledger) == "Ledger föo") - - ttype = TransactionTypeFactory(name='foo') - self.assertEqual(str(ttype), "Transaction Type foo") - - balance = LedgerBalance.objects.last() - self.assertEqual( - str(balance), - "LedgerBalance: %s for %s in %s" % ( - balance.balance, - balance.related_object, - balance.ledger, - ) + ) + + ledger = LedgerFactory(name='foo') + assert str(ledger) == "Ledger foo" + ledger = LedgerFactory(name='föo') + assert str(ledger) == "Ledger föo" + + ttype = TransactionTypeFactory(name='foo') + assert str(ttype) == "Transaction Type foo" + + balance = LedgerBalance.objects.last() + assert str(balance) == ( + "LedgerBalance: %s for %s in %s" % ( + balance.balance, + balance.related_object, + balance.ledger, ) + ) -class TestTransactionSummary(TransactionBase): +def test_transaction_summary(db): """ Test that Transaction.summary returns correct information. """ - def test_transaction_summary(self): - ledger = LedgerFactory() - amount = Decimal('500') - ccx = CreditCardTransactionFactory() - le1 = LedgerEntry(ledger=ledger, amount=credit(amount)) - le2 = LedgerEntry(ledger=ledger, amount=debit(amount)) - txn = TransactionFactory( - evidence=[ccx], - ledger_entries=[le1, le2] - ) - - self.assertEqual( - txn.summary(), - { - 'entries': [str(entry) for entry in txn.entries.all()], - 'related_objects': [ - 'TransactionRelatedObject: CreditCardTransaction(id=%s)' % - ccx.id, - ], - }, - ) - - -class TestSettingExplicitTimestampField(TransactionBase): - def test_setting_explicit_timestamp_field(self): - transaction = TransactionFactory() - old_posted_timestamp = transaction.posted_timestamp - transaction.posted_timestamp = timezone.now() - transaction.save() - self.assertNotEqual( - old_posted_timestamp, - transaction.posted_timestamp, - ) - - -class TestEditingTransactions(TestCase): + ledger = LedgerFactory() + amount = Decimal('500') + ccx = CreditCardTransactionFactory() + le1 = LedgerEntry(ledger=ledger, amount=credit(amount)) + le2 = LedgerEntry(ledger=ledger, amount=debit(amount)) + txn = TransactionFactory( + evidence=[ccx], + ledger_entries=[le1, le2] + ) + + assert txn.summary() == { + 'entries': [str(entry) for entry in txn.entries.all()], + 'related_objects': [ + 'TransactionRelatedObject: CreditCardTransaction(id=%s)' % + ccx.id, + ], + } + + +def test_setting_explicit_timestamp_field(db): + transaction = TransactionFactory() + old_posted_timestamp = transaction.posted_timestamp + transaction.posted_timestamp = timezone.now() + transaction.save() + assert old_posted_timestamp != transaction.posted_timestamp + + +def test_editing_transactions(db): """ Test that validation is still done when editing a Transaction. @@ -118,63 +97,58 @@ class TestEditingTransactions(TestCase): However, we want to make sure that our balance invariants are still kept when editing a Transaction. """ - def test_editing_transactions(self): - transaction = TransactionFactory() + transaction = TransactionFactory() - transaction.notes = 'foo' - transaction.save() + transaction.notes = 'foo' + transaction.save() - entry = transaction.entries.last() - entry.amount += Decimal('1') - entry.save() + entry = transaction.entries.last() + entry.amount += Decimal('1') + entry.save() - with self.assertRaises(TransactionBalanceException): - transaction.save() + with pytest.raises(TransactionBalanceException): + transaction.save() -class TestNonVoidFilter(TestCase): +def test_non_void(db): """ Test Transaction.objects.non_void filter. """ - def setUp(self): - self.order = OrderFactory() - self.ar_ledger = LedgerFactory(name='A/R') - self.cash_ledger = LedgerFactory(name='Cash') - self.user = UserFactory() - def add_transaction(self): + order = OrderFactory() + ar_ledger = LedgerFactory(name='A/R') + cash_ledger = LedgerFactory(name='Cash') + user = UserFactory() + + def add_transaction(): return create_transaction( - self.user, - evidence=[self.order], + user, + evidence=[order], ledger_entries=[ LedgerEntry( - ledger=self.ar_ledger, + ledger=ar_ledger, amount=credit(Decimal(50))), LedgerEntry( - ledger=self.cash_ledger, + ledger=cash_ledger, amount=debit(Decimal(50))), ], ) - def filtered_out_by_non_void(self, transaction): + def filtered_out_by_non_void(transaction): """ Return whether `transaction` is in `Transaction.objects.non_void()`. """ queryset = Transaction.objects.filter(id=transaction.id) - self.assertTrue(queryset.exists()) + assert queryset.exists() return not queryset.non_void().exists() - def test_non_void(self): - """ - Test Transaction.objects.non_void filter. - """ - transaction_1 = self.add_transaction() - self.assertFalse(self.filtered_out_by_non_void(transaction_1)) + transaction_1 = add_transaction() + assert not filtered_out_by_non_void(transaction_1) - transaction_2 = self.add_transaction() - self.assertFalse(self.filtered_out_by_non_void(transaction_2)) + transaction_2 = add_transaction() + assert not filtered_out_by_non_void(transaction_2) - voiding_transaction = void_transaction(transaction_2, self.user) - self.assertFalse(self.filtered_out_by_non_void(transaction_1)) - self.assertTrue(self.filtered_out_by_non_void(transaction_2)) - self.assertTrue(self.filtered_out_by_non_void(voiding_transaction)) + voiding_transaction = void_transaction(transaction_2, user) + assert not filtered_out_by_non_void(transaction_1) + assert filtered_out_by_non_void(transaction_2) + assert filtered_out_by_non_void(voiding_transaction) diff --git a/capone/tests/test_void.py b/capone/tests/test_void.py index e27e194..df3abdf 100644 --- a/capone/tests/test_void.py +++ b/capone/tests/test_void.py @@ -1,6 +1,6 @@ from decimal import Decimal as D -from django.test import TestCase +import pytest from django.utils import timezone from capone.api.actions import create_transaction @@ -15,175 +15,223 @@ from capone.tests.factories import UserFactory -class TestVoidBase(TestCase): - def setUp(self): - self.creation_user = UserFactory() - self.ar_ledger = LedgerFactory() - self.rev_ledger = LedgerFactory() - self.creation_user_ar_ledger = LedgerFactory() - self.ttype = TransactionTypeFactory() - - -class TestVoidTransaction(TestVoidBase): - def test_simple_void(self): - """ - Test voiding a `Transaction`. - """ - amount = D(100) - evidence = UserFactory.create_batch(3) - transaction = create_transaction( - user=UserFactory(), - evidence=evidence, - ledger_entries=[ - LedgerEntry( - ledger=self.ar_ledger, - amount=credit(amount), - ), - LedgerEntry( - ledger=self.rev_ledger, - amount=debit(amount), - ), - ], - ) - self.assertEqual(self.ar_ledger.get_balance(), credit(amount)) - self.assertEqual(self.rev_ledger.get_balance(), debit(amount)) - voiding_transaction = void_transaction(transaction, self.creation_user) - self.assertEqual( - set(tro.related_object for tro - in voiding_transaction.related_objects.all()), - set(evidence), - ) - self.assertEqual(self.ar_ledger.get_balance(), D(0)) - self.assertEqual(self.rev_ledger.get_balance(), D(0)) - self.assertEqual(voiding_transaction.voids, transaction) - self.assertEqual( - voiding_transaction.posted_timestamp, - transaction.posted_timestamp) - self.assertEqual( - voiding_transaction.type, - transaction.type) - self.assertEqual( - voiding_transaction.notes, - 'Voiding transaction {}'.format(transaction)) - - def test_void_with_non_default_type(self): - """ - Test voiding a `Transaction` with a non-default `type`. - """ - amount = D(100) - txn = TransactionFactory(self.creation_user, ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=self.ar_ledger), - LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), - ]) - - new_ttype = TransactionTypeFactory() - void_txn = void_transaction(txn, self.creation_user, type=new_ttype) - - self.assertEqual(void_txn.voids, txn) - - self.assertEqual(self.ar_ledger.get_balance(), D(0)) - self.assertEqual(self.rev_ledger.get_balance(), D(0)) - - self.assertEqual(void_txn.type, new_ttype) - self.assertNotEqual(void_txn.type, txn.type) - - def test_cant_void_twice(self): - """ - Voiding a `Transaction` more than once is not permitted. - """ - amount = D(100) - txn = TransactionFactory(self.creation_user, ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=self.ar_ledger), - LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), - ]) - - void_transaction(txn, self.creation_user) - - self.assertRaises( - UnvoidableTransactionException, - void_transaction, txn, self.creation_user) - - def test_can_void_void(self): - """ - A void can be voided, thus restoring the original transaction. - """ - amount = D(100) - txn = TransactionFactory(self.creation_user, ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=self.ar_ledger), - LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), - ]) - - void_txn = void_transaction(txn, self.creation_user) - - self.assertEqual(void_txn.voids, txn) - - void_void_txn = (void_transaction(void_txn, self.creation_user)) - self.assertEqual(void_void_txn.voids, void_txn) - - self.assertEqual(self.ar_ledger.get_balance(), amount) - self.assertEqual(self.rev_ledger.get_balance(), -amount) - - def test_void_with_overridden_notes_and_type(self): - """ - Test voiding while setting notes and type. - """ - amount = D(100) - evidence = UserFactory.create_batch(3) - transaction = create_transaction( - user=UserFactory(), - evidence=evidence, - ledger_entries=[ - LedgerEntry( - ledger=self.ar_ledger, - amount=credit(amount), - ), - LedgerEntry( - ledger=self.rev_ledger, - amount=debit(amount), - ), - ], - type=self.ttype, - ) - voiding_transaction = void_transaction( - transaction, - self.creation_user, - notes='test notes', - ) - self.assertEqual(voiding_transaction.notes, 'test notes') - self.assertEqual(voiding_transaction.type, transaction.type) - - -class TestVoidTimestamps(TestVoidBase): +amount = D(100) + + +@pytest.fixture +def create_objects(db): + creation_user = UserFactory() + ar_ledger = LedgerFactory() + rev_ledger = LedgerFactory() + creation_user_ar_ledger = LedgerFactory() + ttype = TransactionTypeFactory() + return ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) + + +def test_simple_void(create_objects): + """ + Test voiding a `Transaction`. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + evidence = UserFactory.create_batch(3) + transaction = create_transaction( + user=UserFactory(), + evidence=evidence, + ledger_entries=[ + LedgerEntry( + ledger=ar_ledger, + amount=credit(amount), + ), + LedgerEntry( + ledger=rev_ledger, + amount=debit(amount), + ), + ], + ) + assert ar_ledger.get_balance() == credit(amount) + assert rev_ledger.get_balance() == debit(amount) + voiding_transaction = void_transaction(transaction, creation_user) + assert set( + tro.related_object for tro in voiding_transaction.related_objects.all() + ) == set(evidence) + assert ar_ledger.get_balance() == D(0) + assert rev_ledger.get_balance() == D(0) + assert voiding_transaction.voids == transaction + assert voiding_transaction.posted_timestamp == transaction.posted_timestamp + assert voiding_transaction.type == transaction.type + assert voiding_transaction.notes == 'Voiding transaction {}'.format( + transaction, + ) + + +def test_void_with_non_default_type(create_objects): + """ + Test voiding a `Transaction` with a non-default `type`. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + txn = TransactionFactory(creation_user, ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ar_ledger), + LedgerEntry(amount=credit(amount), ledger=rev_ledger), + ]) + + new_ttype = TransactionTypeFactory() + void_txn = void_transaction(txn, creation_user, type=new_ttype) + + assert void_txn.voids == txn + + assert ar_ledger.get_balance() == D(0) + assert rev_ledger.get_balance() == D(0) + + assert void_txn.type == new_ttype + assert void_txn.type != txn.type + + +def test_cant_void_twice(create_objects): + """ + Voiding a `Transaction` more than once is not permitted. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + txn = TransactionFactory(creation_user, ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ar_ledger), + LedgerEntry(amount=credit(amount), ledger=rev_ledger), + ]) + + void_transaction(txn, creation_user) + + pytest.raises( + UnvoidableTransactionException, + void_transaction, + txn, + creation_user, + ) + + +def test_can_void_void(create_objects): + """ + A void can be voided, thus restoring the original transaction. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + txn = TransactionFactory(creation_user, ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ar_ledger), + LedgerEntry(amount=credit(amount), ledger=rev_ledger), + ]) + + void_txn = void_transaction(txn, creation_user) + + assert void_txn.voids == txn + + void_void_txn = (void_transaction(void_txn, creation_user)) + assert void_void_txn.voids == void_txn + + assert ar_ledger.get_balance() == amount + assert rev_ledger.get_balance() == -amount + + +def test_void_with_overridden_notes_and_type(create_objects): + """ + Test voiding while setting notes and type. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + evidence = UserFactory.create_batch(3) + transaction = create_transaction( + user=UserFactory(), + evidence=evidence, + ledger_entries=[ + LedgerEntry( + ledger=ar_ledger, + amount=credit(amount), + ), + LedgerEntry( + ledger=rev_ledger, + amount=debit(amount), + ), + ], + type=ttype, + ) + voiding_transaction = void_transaction( + transaction, + creation_user, + notes='test notes', + ) + assert voiding_transaction.notes == 'test notes' + assert voiding_transaction.type == transaction.type + + +def test_auto_timestamp(create_objects): + """ + If a posted_timestamp isn't specified we assume the posted_timestamp is + the same as the transaction we're voiding. + """ + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + charge_txn = TransactionFactory(creation_user, ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ar_ledger), + LedgerEntry(amount=credit(amount), ledger=rev_ledger), + ]) + + void_txn = void_transaction(charge_txn, creation_user) + assert charge_txn.posted_timestamp == void_txn.posted_timestamp + + +def test_given_timestamp(create_objects): """ - Test automatic and manual handling of `posted_timestamp` on voids. + If a posted_timestamp is given for the void, then use it """ - def test_auto_timestamp(self): - """ - If a posted_timestamp isn't specified we assume the posted_timestamp is - the same as the transaction we're voiding. - """ - amount = D(100) - charge_txn = TransactionFactory(self.creation_user, ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=self.ar_ledger), - LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), - ]) - - void_txn = void_transaction(charge_txn, self.creation_user) - self.assertEqual( - charge_txn.posted_timestamp, void_txn.posted_timestamp) - - def test_given_timestamp(self): - """ - If a posted_timestamp is given for the void, then use it - """ - amount = D(100) - charge_txn = TransactionFactory(self.creation_user, ledger_entries=[ - LedgerEntry(amount=debit(amount), ledger=self.ar_ledger), - LedgerEntry(amount=credit(amount), ledger=self.rev_ledger), - ]) - - now = timezone.now() - void_txn = void_transaction( - charge_txn, self.creation_user, - posted_timestamp=now) - self.assertEqual(now, void_txn.posted_timestamp) + ( + creation_user, + ar_ledger, + rev_ledger, + creation_user_ar_ledger, + ttype, + ) = create_objects + charge_txn = TransactionFactory(creation_user, ledger_entries=[ + LedgerEntry(amount=debit(amount), ledger=ar_ledger), + LedgerEntry(amount=credit(amount), ledger=rev_ledger), + ]) + + now = timezone.now() + void_txn = void_transaction( + charge_txn, creation_user, + posted_timestamp=now) + assert now == void_txn.posted_timestamp From bc5e5fc444757ecd1d2e39f6191277d975c30496 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 26 Aug 2024 23:42:07 -0500 Subject: [PATCH 17/30] Parametrize test suite on USE_TZ --- conftest.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..3bf4d2a --- /dev/null +++ b/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + """ + Enable database access for all tests. + + PyTest doesn't allow database access by default, so we create a fixture with `autouse=True` at + the top level of the project to automatically give all tests access to the database. As we + extend the use of PyTest, especially to "database disallowed" tests, and as we clean up and + make more apparent the dependencies of each test, we can narrow the scope of this fixture, + eventually removing it. + + See https://pytest-django.readthedocs.io/en/latest/faq.html?highlight=enable_db_access_for_all_tests#how-can-i-give-database-access-to-all-my-tests-without-the-django-db-marker. + """ + + +@pytest.fixture(autouse=True, params=[False, True]) +def parametrize_entire_test_suite_by_use_tz(request, settings, db): + settings.USE_TZ = request.param From b091857ba8d56adcb4bd889a8e150ea16d2659dd Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 09:02:53 -0500 Subject: [PATCH 18/30] Remove now-unnecessary db fixture --- ...saction_in_ledgers_for_amounts_with_evidence.py | 10 +++++----- capone/tests/test_create_transaction.py | 14 +++++++------- capone/tests/test_factories.py | 4 ++-- capone/tests/test_filter_by_related_objects.py | 14 +++++--------- capone/tests/test_ledger_balances.py | 4 ++-- capone/tests/test_transaction_model.py | 10 +++++----- capone/tests/test_void.py | 2 +- 7 files changed, 27 insertions(+), 31 deletions(-) diff --git a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py index 4295f87..994b5ff 100644 --- a/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py +++ b/capone/tests/test_assert_transaction_in_ledgers_for_amounts_with_evidence.py @@ -17,7 +17,7 @@ from capone.tests.factories import UserFactory -def test_transaction_fields(db): +def test_transaction_fields(): """ Test filtering by `posted_timestamp`, `notes`, `type`, and `user`. """ @@ -51,7 +51,7 @@ def test_transaction_fields(db): ) -def test_no_matches(db): +def test_no_matches(): """ No matching transaction raises DoesNotExist. """ @@ -71,7 +71,7 @@ def test_no_matches(db): ) -def test_multiple_matches(db): +def test_multiple_matches(): """ Multiple matching transactions raises MultipleObjectsReturned. """ @@ -100,7 +100,7 @@ def test_multiple_matches(db): ) -def test_mismatch_on_ledger_entries(db): +def test_mismatch_on_ledger_entries(): """ An otherwise matching Trans. will fail if its LedgerEntries mismatch. """ @@ -137,7 +137,7 @@ def test_mismatch_on_ledger_entries(db): ) -def test_mismatch_on_evidence(db): +def test_mismatch_on_evidence(): """ An otherwise matching Trans. will fail if its evidence is different. """ diff --git a/capone/tests/test_create_transaction.py b/capone/tests/test_create_transaction.py index d9281b6..979ddb1 100644 --- a/capone/tests/test_create_transaction.py +++ b/capone/tests/test_create_transaction.py @@ -24,7 +24,7 @@ @pytest.fixture -def create_objects(db): +def create_objects(): user = UserFactory() accounts_receivable = LedgerFactory(name='Accounts Receivable') cash_unrecon = LedgerFactory(name='Cash (unreconciled)') @@ -219,7 +219,7 @@ def test_no_ledger_entries(create_objects): validate_transaction(user) -def test_with_existing_ledger_entry(db): +def test_with_existing_ledger_entry(): amount = D(100) user = UserFactory() @@ -293,26 +293,26 @@ def _create_transaction_and_compare_to_amount( assert entry2.amount == -amount -def test_precision(db): +def test_precision(): _create_transaction_and_compare_to_amount( D('-499.9999')) -def test_round_up(db): +def test_round_up(): _create_transaction_and_compare_to_amount( D('499.99995'), D('500')) -def test_round_down(db): +def test_round_down(): _create_transaction_and_compare_to_amount( D('499.99994'), D('499.9999')) -def test_round_up_negative(db): +def test_round_up_negative(): _create_transaction_and_compare_to_amount( D('-499.99994'), D('-499.9999')) -def test_round_down_negative(db): +def test_round_down_negative(): _create_transaction_and_compare_to_amount( D('-499.99995'), D('-500')) diff --git a/capone/tests/test_factories.py b/capone/tests/test_factories.py index 79bb460..3a72db2 100644 --- a/capone/tests/test_factories.py +++ b/capone/tests/test_factories.py @@ -24,7 +24,7 @@ @pytest.fixture -def credit_card_transaction(db): +def credit_card_transaction(): return CreditCardTransactionFactory() @@ -61,7 +61,7 @@ def test_custom_ledger_entries(credit_card_transaction): ) -def test_custom_evidence(db): +def test_custom_evidence(): ccx = CreditCardTransactionFactory() TransactionFactory(evidence=[ccx]) diff --git a/capone/tests/test_filter_by_related_objects.py b/capone/tests/test_filter_by_related_objects.py index f9451d2..c428dfb 100644 --- a/capone/tests/test_filter_by_related_objects.py +++ b/capone/tests/test_filter_by_related_objects.py @@ -20,7 +20,7 @@ @pytest.fixture -def create_transactions(db): +def create_transactions(): create_user = UserFactory() ledger = LedgerFactory() @@ -90,7 +90,7 @@ def _create_transaction_with_evidence(evidence): (MatchType.EXACT, 'none'), ]) def test_filter_with_no_evidence( - match_type, queryset_function_name, create_transactions, db, + match_type, queryset_function_name, create_transactions, ): """ Method returns correct Transactions with no evidence given. @@ -109,7 +109,7 @@ def test_filter_with_no_evidence( (MatchType.NONE, [False, False, False, True, False]), (MatchType.EXACT, [True, False, False, False, False]), ]) -def test_filters(match_type, results, create_transactions, db): +def test_filters(match_type, results, create_transactions): """ Method returns correct Transactions with various evidence given. @@ -156,11 +156,7 @@ def test_filters(match_type, results, create_transactions, db): (MatchType.EXACT, 4), ]) def test_query_counts( - match_type, - query_counts, - django_assert_num_queries, - create_transactions, - db, + match_type, query_counts, django_assert_num_queries, create_transactions, ): """ `filter_by_related_objects` should use a constant number of queries. @@ -187,7 +183,7 @@ def test_invalid_match_type(): Transaction.objects.filter_by_related_objects(match_type='foo') -def test_chaining_filter_to_existing_queryset(create_transactions, db): +def test_chaining_filter_to_existing_queryset(create_transactions): """ `filter_by_related_objects` can be used like any other queryset filter. """ diff --git a/capone/tests/test_ledger_balances.py b/capone/tests/test_ledger_balances.py index 1cbd752..7a9242f 100644 --- a/capone/tests/test_ledger_balances.py +++ b/capone/tests/test_ledger_balances.py @@ -28,7 +28,7 @@ @pytest.fixture -def create_objects(db): +def create_objects(): order_1, order_2 = OrderFactory.create_batch(2) ar_ledger = LedgerFactory(name='A/R') cash_ledger = LedgerFactory(name='Cash') @@ -270,7 +270,7 @@ def add_transaction(orders): ) -def test_ledger_balances_filtering(db, create_objects): +def test_ledger_balances_filtering(create_objects): ( order_1, order_2, diff --git a/capone/tests/test_transaction_model.py b/capone/tests/test_transaction_model.py index 59b376f..0edb597 100644 --- a/capone/tests/test_transaction_model.py +++ b/capone/tests/test_transaction_model.py @@ -20,7 +20,7 @@ from capone.tests.factories import UserFactory -def test_unicode_methods(db): +def test_unicode_methods(): """ Test all __str__ methods. """ @@ -58,7 +58,7 @@ def test_unicode_methods(db): ) -def test_transaction_summary(db): +def test_transaction_summary(): """ Test that Transaction.summary returns correct information. """ @@ -81,7 +81,7 @@ def test_transaction_summary(db): } -def test_setting_explicit_timestamp_field(db): +def test_setting_explicit_timestamp_field(): transaction = TransactionFactory() old_posted_timestamp = transaction.posted_timestamp transaction.posted_timestamp = timezone.now() @@ -89,7 +89,7 @@ def test_setting_explicit_timestamp_field(db): assert old_posted_timestamp != transaction.posted_timestamp -def test_editing_transactions(db): +def test_editing_transactions(): """ Test that validation is still done when editing a Transaction. @@ -110,7 +110,7 @@ def test_editing_transactions(db): transaction.save() -def test_non_void(db): +def test_non_void(): """ Test Transaction.objects.non_void filter. """ diff --git a/capone/tests/test_void.py b/capone/tests/test_void.py index df3abdf..a3b60bd 100644 --- a/capone/tests/test_void.py +++ b/capone/tests/test_void.py @@ -19,7 +19,7 @@ @pytest.fixture -def create_objects(db): +def create_objects(): creation_user = UserFactory() ar_ledger = LedgerFactory() rev_ledger = LedgerFactory() From ade8eb3bfa65dca4e0b29d7cad19508a6ad7d2d8 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:38:36 -0500 Subject: [PATCH 19/30] Remove copypasta --- .github/workflows/django.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 3e641c5..5d03893 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -42,7 +42,6 @@ jobs: - name: Install Dependencies run: | pip install virtualenv - npm i pg - name: Run Tests run: | make test From df9b2454f03940742acebcfa94ea56a8103cd0ab Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:39:38 -0500 Subject: [PATCH 20/30] Remove virtualenv --- .github/workflows/django.yml | 3 --- Makefile | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 5d03893..737166a 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -39,9 +39,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - pip install virtualenv - name: Run Tests run: | make test diff --git a/Makefile b/Makefile index 679a26e..8e2b38a 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ VENDOR_SENTINEL:=.sentinel venv: $(VENV_ACTIVATE) $(VENV_ACTIVATE): requirements*.txt - test -f $@ || virtualenv --python=python3.6 $(VENV_DIR) + test -f $@ || python -m venv $(VENV_DIR) $(WITH_VENV) pip install -r requirements-setup.txt $(WITH_VENV) pip install -e . $(WITH_VENV) pip install -r requirements-dev.txt From 4dd9a128331fcf023e2948ec208f65b9ac4de1ce Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:41:35 -0500 Subject: [PATCH 21/30] Fix passenv formatting --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3748d21..e82485c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ install_command = pip install {opts} {packages} envlist = {py36,py38}-{1.11,2.2,3.2,4.2}, flake8 [testenv] -passenv = LANG POSTGRES_HOST POSTGRES_DB POSTGRES_PASSWORD POSTGRES_PORT POSTGRES_USER +passenv = LANG,POSTGRES_HOST,POSTGRES_DB,POSTGRES_PASSWORD,POSTGRES_PORT,POSTGRES_USER usedevelop = True setenv = TOXENV={envname} From 724f44982a1788134ce7c5d178ded25de8b5b06d Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 17:04:58 -0500 Subject: [PATCH 22/30] Reformat passenv --- tox.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e82485c..b0faf20 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,13 @@ install_command = pip install {opts} {packages} envlist = {py36,py38}-{1.11,2.2,3.2,4.2}, flake8 [testenv] -passenv = LANG,POSTGRES_HOST,POSTGRES_DB,POSTGRES_PASSWORD,POSTGRES_PORT,POSTGRES_USER +passenv = + LANG + POSTGRES_HOST + POSTGRES_DB + POSTGRES_PASSWORD + POSTGRES_PORT + POSTGRES_USER usedevelop = True setenv = TOXENV={envname} From 749e05ccecc7c12524513f43856ec87f7924d78f Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:37:13 -0500 Subject: [PATCH 23/30] Revert "Only enable running versions for now" This reverts commit fd7e9c3bfec4093bd11a54e6e23f093d97b8b28a. --- .python-version | 1 + tox.ini | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.python-version b/.python-version index a168fc7..807c3e5 100644 --- a/.python-version +++ b/.python-version @@ -1,2 +1,3 @@ 3.6 3.8 +3.9 diff --git a/tox.ini b/tox.ini index b0faf20..21e2a62 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] install_command = pip install {opts} {packages} -envlist = {py36,py38}-{1.11,2.2,3.2,4.2}, flake8 +envlist = {py36,py38,py39}-{1.11,2.2,3.2,4.2}, flake8 [testenv] passenv = @@ -55,6 +55,18 @@ deps = Django>=1.11,<2 psycopg2>=2.6.2,<2.9 +[testenv:py39-1.11] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + +[testenv:py39-2.2] +deps = + -r{toxinidir}/requirements-dev.txt + Django>=1.11,<2 + psycopg2>=2.6.2,<2.9 + [testenv:flake8] commands = {envbindir}/flake8 -v capone From 6579c417f9b3084a660d72a8a71283ffbd2f1328 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 12:37:42 -0500 Subject: [PATCH 24/30] Add other versions separately --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 737166a..0b925ee 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6] + python-version: [3.6, 3.8, 3.9] steps: - uses: actions/checkout@v3 From 9179ebf56ca13ac6c0e0967fcf4cf5e69a6ac7b5 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Tue, 27 Aug 2024 10:07:15 -0500 Subject: [PATCH 25/30] Rework pytest calls and silence datetime warning --- tox.ini | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 21e2a62..b605974 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -install_command = pip install {opts} {packages} envlist = {py36,py38,py39}-{1.11,2.2,3.2,4.2}, flake8 [testenv] @@ -15,15 +14,8 @@ setenv = TOXENV={envname} XUNIT_FILE=pytest-{envname}.xml commands = - {envbindir}/pytest \ - --cov=capone\ - --cov-append \ - --cov-report xml:coverage/coverage.xml \ - --cov-report html:coverage/ \ - --cov-fail-under 100 \ - --junitxml=$XUNIT_FILE \ - -v -rx \ - capone + pytest --cov=capone --cov-fail-under 100 {posargs} + deps = -r{toxinidir}/requirements-dev.txt 1.11: Django>=1.11,<2 @@ -68,7 +60,7 @@ deps = psycopg2>=2.6.2,<2.9 [testenv:flake8] -commands = {envbindir}/flake8 -v capone +commands = flake8 -v capone [pytest] DJANGO_SETTINGS_MODULE = capone.tests.settings From 2254bf708f94f02790ed770138c61beacc89e787 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Wed, 28 Aug 2024 10:05:23 -0500 Subject: [PATCH 26/30] Update setup.cfg --- setup.cfg | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index c55f01b..2a45018 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,12 @@ name = capone author = Hunter Richards author_email = opensource@counsyl.com -summary = Django app representing a double-entry accounting ledger. -description_file = README.rst -home_page = https://github.com/counsyl/capone -license = Copyright Counsyl, Inc. -classifier = +description = Django app representing a double-entry accounting ledger. +long_description = file: README.rst +long_description_content_type = text/x-rst +url = https://github.com/counsyl/capone +license = MIT +classifiers = Development Status :: 5 - Production/Stable Framework :: Django Intended Audience :: Developers @@ -16,15 +17,18 @@ classifier = License :: OSI Approved :: MIT License Natural Language :: English Operating System :: OS Independent - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Office/Business :: Financial :: Accounting -[files] -packages = capone + [pbr] skip_authors = true skip_changelog = true + +[options] +packages = find: + [bdist_wheel] universal = 1 From a406fa1d39bcf5871882984f6ec1d8eedb1a270f Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Wed, 28 Aug 2024 10:25:00 -0500 Subject: [PATCH 27/30] Add `wheel` to setup_requires --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aa10fa0..20c20e6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup setup( - setup_requires=['pbr'], + setup_requires=['pbr', 'wheel'], pbr=True, ) From 3d635f6069e951fd40503e99f491cf5d33422414 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Wed, 28 Aug 2024 10:30:48 -0500 Subject: [PATCH 28/30] Pin wheel --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20c20e6..91a8694 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup setup( - setup_requires=['pbr', 'wheel'], + setup_requires=['pbr', 'wheel==0.37.1'], pbr=True, ) From 28e2a7cb94e635d2bed805185fa74ef6e5832c76 Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Wed, 28 Aug 2024 10:40:58 -0500 Subject: [PATCH 29/30] Skip missing interpreters This is useful only for tox<4 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index b605974..dd7962b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] envlist = {py36,py38,py39}-{1.11,2.2,3.2,4.2}, flake8 +skip_missing_interpreters = + true [testenv] passenv = From 716aac666b2a093c30fef65efe27b5f22e220b3c Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Wed, 28 Aug 2024 10:48:17 -0500 Subject: [PATCH 30/30] Sem-ver: feature