diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 492080b..0b925ee 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 @@ -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 @@ -39,10 +39,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - pip install virtualenv - npm i pg - name: Run Tests run: | make test 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 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. 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 diff --git a/capone/models.py b/capone/models.py index 9d3fa70..abad1e1 100644 --- a/capone/models.py +++ b/capone/models.py @@ -10,8 +10,7 @@ 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 django.utils.translation import gettext_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`. @@ -267,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): @@ -292,7 +289,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 +364,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/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 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..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 @@ -1,8 +1,8 @@ -from datetime import datetime 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 from capone.api.actions import debit @@ -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 = datetime.now() - wrong_time = datetime.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(): + """ + 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(): + """ + 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(): + """ + 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(): + """ + 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(): + """ + 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 538f177..979ddb1 100644 --- a/capone/tests/test_create_transaction.py +++ b/capone/tests/test_create_transaction.py @@ -1,8 +1,7 @@ -from datetime import datetime 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 from capone.exceptions import NoLedgerEntriesException @@ -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(): + 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(): + 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 = datetime(2016, 2, 7, 11, 59) - 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(): + _create_transaction_and_compare_to_amount( + D('-499.9999')) + + +def test_round_up(): + _create_transaction_and_compare_to_amount( + D('499.99995'), D('500')) + + +def test_round_down(): + _create_transaction_and_compare_to_amount( + D('499.99994'), D('499.9999')) + + +def test_round_up_negative(): + _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(): + _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 47e7362..3a72db2 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 +import pytest +from django.utils import timezone from capone.api.actions import credit from capone.api.actions import debit @@ -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(): + 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(): + 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 = datetime.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..c428dfb 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,201 @@ 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(): + 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, +): + """ + 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): + """ + 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, +): + """ + `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): + """ + `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..7a9242f 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(): + 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(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 307a620..0edb597 100644 --- a/capone/tests/test_transaction_model.py +++ b/capone/tests/test_transaction_model.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import sys -from datetime import datetime from decimal import Decimal -from django.test import TestCase +import pytest +from django.utils import timezone from capone.api.actions import create_transaction from capone.api.actions import credit @@ -21,100 +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 = datetime.now() - - -class TestStrMethods(TestCase): +def test_unicode_methods(): """ 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') - 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") - - 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(): """ 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 = datetime.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(): + 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(): """ Test that validation is still done when editing a Transaction. @@ -122,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(): """ 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 60d3802..a3b60bd 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 +import pytest +from django.utils import timezone from capone.api.actions import create_transaction from capone.api.actions import credit @@ -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(): + 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 = datetime.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 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 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 diff --git a/requirements.txt b/requirements.txt index 9a64921..aefd0b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ psycopg2>=2.6.2,<3 -Django>=1.11,<3 -enum34 +Django>=1.11,<5 diff --git a/setup.cfg b/setup.cfg index 1129a29..2a45018 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,13 @@ [metadata] 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 = +author_email = opensource@counsyl.com +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 diff --git a/setup.py b/setup.py index aa10fa0..91a8694 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup setup( - setup_requires=['pbr'], + setup_requires=['pbr', 'wheel==0.37.1'], pbr=True, ) diff --git a/tox.ini b/tox.ini index 7c72b00..dd7962b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,33 +1,73 @@ [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 +skip_missing_interpreters = + true [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} 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 2.2: Django>=2.2,<3 + 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 +commands = flake8 -v capone [pytest] DJANGO_SETTINGS_MODULE = capone.tests.settings +filterwarnings = + ignore: DateTimeField .* received a naive datetime:RuntimeWarning:: [flake8] ignore = W503