mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-01-11 20:00:26 +01:00
311 lines
14 KiB
Python
311 lines
14 KiB
Python
from django.test import TestCase, Client
|
|
from django.core.exceptions import ValidationError
|
|
from django.contrib.auth.models import User
|
|
from django.db import IntegrityError, models
|
|
from django.utils import timezone
|
|
from django.urls import reverse
|
|
from decimal import Decimal
|
|
|
|
from apps.accounts.models import Account, AccountGroup
|
|
from apps.currencies.models import Currency
|
|
from apps.accounts.forms import AccountForm
|
|
from apps.transactions.models import Transaction, TransactionCategory
|
|
|
|
|
|
class AccountTests(TestCase):
|
|
def setUp(self):
|
|
"""Set up test data"""
|
|
self.owner1 = User.objects.create_user(username='testowner', password='password123')
|
|
self.client = Client()
|
|
self.client.login(username='testowner', password='password123')
|
|
|
|
self.currency = Currency.objects.create(
|
|
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
|
|
)
|
|
self.eur = Currency.objects.create(
|
|
code="EUR", name="Euro", decimal_places=2, prefix="€ "
|
|
)
|
|
self.account_group = AccountGroup.objects.create(name="Test Group", owner=self.owner1)
|
|
self.reconciliation_category = TransactionCategory.objects.create(name='Reconciliation', owner=self.owner1, type='INFO')
|
|
|
|
|
|
def test_account_creation(self):
|
|
"""Test basic account creation"""
|
|
account = Account.objects.create(
|
|
name="Test Account",
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
is_asset=False,
|
|
is_archived=False,
|
|
)
|
|
self.assertEqual(str(account), "Test Account")
|
|
self.assertEqual(account.name, "Test Account")
|
|
self.assertEqual(account.group, self.account_group)
|
|
self.assertEqual(account.currency, self.currency)
|
|
self.assertFalse(account.is_asset)
|
|
self.assertFalse(account.is_archived)
|
|
|
|
def test_account_with_exchange_currency(self):
|
|
"""Test account creation with exchange currency"""
|
|
account = Account.objects.create(
|
|
name="Exchange Account",
|
|
owner=self.owner1, # Added owner
|
|
group=self.account_group, # Added group
|
|
currency=self.currency,
|
|
exchange_currency=self.eur, # Changed to self.eur
|
|
)
|
|
self.assertEqual(account.exchange_currency, self.eur) # Changed to self.eur
|
|
|
|
def test_account_archiving(self):
|
|
"""Test archiving and unarchiving an account"""
|
|
account = Account.objects.create(
|
|
name="Archivable Account",
|
|
owner=self.owner1, # Added owner
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
is_asset=True, # Assuming default, can be anything for this test
|
|
is_archived=False,
|
|
)
|
|
self.assertFalse(account.is_archived, "Account should initially be unarchived")
|
|
|
|
# Archive the account
|
|
account.is_archived = True
|
|
account.save()
|
|
|
|
archived_account = Account.objects.get(pk=account.pk)
|
|
self.assertTrue(archived_account.is_archived, "Account should be archived")
|
|
|
|
# Unarchive the account
|
|
archived_account.is_archived = False
|
|
archived_account.save()
|
|
|
|
unarchived_account = Account.objects.get(pk=account.pk)
|
|
self.assertFalse(unarchived_account.is_archived, "Account should be unarchived")
|
|
|
|
def test_account_exchange_currency_cannot_be_same_as_currency(self):
|
|
"""Test that exchange_currency cannot be the same as currency."""
|
|
with self.assertRaises(ValidationError) as cm:
|
|
account = Account(
|
|
name="Same Currency Account",
|
|
owner=self.owner1, # Added owner
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
exchange_currency=self.currency, # Same as currency
|
|
)
|
|
account.full_clean()
|
|
self.assertIn('exchange_currency', cm.exception.error_dict)
|
|
# To check for a specific message (optional, might make test brittle):
|
|
# self.assertTrue(any("cannot be the same as the main currency" in e.message
|
|
# for e in cm.exception.error_dict['exchange_currency']))
|
|
|
|
def test_account_name_unique_per_owner(self):
|
|
"""Test that account name is unique per owner."""
|
|
owner1 = User.objects.create_user(username='owner1', password='password123')
|
|
owner2 = User.objects.create_user(username='owner2', password='password123')
|
|
|
|
# Initial account for self.owner1 (owner1 from setUp)
|
|
Account.objects.create(
|
|
name="Unique Name Test",
|
|
owner=self.owner1, # Changed to self.owner1
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
)
|
|
|
|
# Attempt to create another account with the same name and self.owner1 - should fail
|
|
with self.assertRaises(IntegrityError):
|
|
Account.objects.create(
|
|
name="Unique Name Test",
|
|
owner=self.owner1, # Changed to self.owner1
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
)
|
|
|
|
# Create account with the same name but for owner2 - should succeed
|
|
try:
|
|
Account.objects.create(
|
|
name="Unique Name Test",
|
|
owner=owner2, # owner2 is locally defined here, that's fine for this test
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
)
|
|
except IntegrityError:
|
|
self.fail("Creating account with same name but different owner failed unexpectedly.")
|
|
|
|
# Create account with a different name for self.owner1 - should succeed
|
|
try:
|
|
Account.objects.create(
|
|
name="Another Name Test",
|
|
owner=self.owner1, # Changed to self.owner1
|
|
group=self.account_group,
|
|
currency=self.currency,
|
|
)
|
|
except IntegrityError:
|
|
self.fail("Creating account with different name for the same owner failed unexpectedly.")
|
|
|
|
def test_account_form_valid_data(self):
|
|
"""Test AccountForm with valid data."""
|
|
form_data = {
|
|
'name': 'Form Test Account',
|
|
'group': self.account_group.pk,
|
|
'currency': self.currency.pk,
|
|
'exchange_currency': self.eur.pk,
|
|
'is_asset': True,
|
|
'is_archived': False,
|
|
'description': 'A valid test account from form.'
|
|
}
|
|
form = AccountForm(data=form_data)
|
|
self.assertTrue(form.is_valid(), form.errors.as_text())
|
|
|
|
account = form.save(commit=False)
|
|
account.owner = self.owner1
|
|
account.save()
|
|
|
|
self.assertEqual(account.name, 'Form Test Account')
|
|
self.assertEqual(account.owner, self.owner1)
|
|
self.assertEqual(account.group, self.account_group)
|
|
self.assertEqual(account.currency, self.currency)
|
|
self.assertEqual(account.exchange_currency, self.eur)
|
|
self.assertTrue(account.is_asset)
|
|
self.assertFalse(account.is_archived)
|
|
|
|
def test_account_form_missing_name(self):
|
|
"""Test AccountForm with missing name."""
|
|
form_data = {
|
|
'group': self.account_group.pk,
|
|
'currency': self.currency.pk,
|
|
}
|
|
form = AccountForm(data=form_data)
|
|
self.assertFalse(form.is_valid())
|
|
self.assertIn('name', form.errors)
|
|
|
|
def test_account_form_exchange_currency_same_as_currency(self):
|
|
"""Test AccountForm where exchange_currency is the same as currency."""
|
|
form_data = {
|
|
'name': 'Same Currency Form Account',
|
|
'group': self.account_group.pk,
|
|
'currency': self.currency.pk,
|
|
'exchange_currency': self.currency.pk, # Same as currency
|
|
}
|
|
form = AccountForm(data=form_data)
|
|
self.assertFalse(form.is_valid())
|
|
self.assertIn('exchange_currency', form.errors)
|
|
|
|
|
|
class AccountGroupTests(TestCase):
|
|
def setUp(self):
|
|
"""Set up test data for AccountGroup tests."""
|
|
self.owner1 = User.objects.create_user(username='groupowner1', password='password123')
|
|
self.owner2 = User.objects.create_user(username='groupowner2', password='password123')
|
|
|
|
def test_account_group_creation(self):
|
|
"""Test basic AccountGroup creation."""
|
|
group = AccountGroup.objects.create(name="Test Group", owner=self.owner1)
|
|
self.assertEqual(group.name, "Test Group")
|
|
self.assertEqual(group.owner, self.owner1)
|
|
self.assertEqual(str(group), "Test Group") # Assuming __str__ returns the name
|
|
|
|
def test_account_group_name_unique_per_owner(self):
|
|
"""Test that AccountGroup name is unique per owner."""
|
|
# Initial group for owner1
|
|
AccountGroup.objects.create(name="Unique Group Name", owner=self.owner1)
|
|
|
|
# Attempt to create another group with the same name and owner1 - should fail
|
|
with self.assertRaises(IntegrityError):
|
|
AccountGroup.objects.create(name="Unique Group Name", owner=self.owner1)
|
|
|
|
# Create group with the same name but for owner2 - should succeed
|
|
try:
|
|
AccountGroup.objects.create(name="Unique Group Name", owner=self.owner2)
|
|
except IntegrityError:
|
|
self.fail("Creating group with same name but different owner failed unexpectedly.")
|
|
|
|
# Create group with a different name for owner1 - should succeed
|
|
try:
|
|
AccountGroup.objects.create(name="Another Group Name", owner=self.owner1)
|
|
except IntegrityError:
|
|
self.fail("Creating group with different name for the same owner failed unexpectedly.")
|
|
|
|
def test_account_reconciliation_creates_transaction(self):
|
|
"""Test that account_reconciliation view creates a transaction for the difference."""
|
|
|
|
# Helper function to get balance
|
|
def get_balance(account):
|
|
balance = account.transactions.filter(is_paid=True).aggregate(
|
|
total_income=models.Sum('amount', filter=models.Q(type=Transaction.Type.INCOME)),
|
|
total_expense=models.Sum('amount', filter=models.Q(type=Transaction.Type.EXPENSE)),
|
|
total_transfer_in=models.Sum('amount', filter=models.Q(type=Transaction.Type.TRANSFER, transfer_to_account=account)),
|
|
total_transfer_out=models.Sum('amount', filter=models.Q(type=Transaction.Type.TRANSFER, account=account))
|
|
)['total_income'] or Decimal('0.00')
|
|
balance -= account.transactions.filter(is_paid=True).aggregate(
|
|
total_expense=models.Sum('amount', filter=models.Q(type=Transaction.Type.EXPENSE))
|
|
)['total_expense'] or Decimal('0.00')
|
|
# For transfers, a more complete logic might be needed if transfers are involved in reconciliation scope
|
|
return balance
|
|
|
|
account_usd = Account.objects.create(
|
|
name="USD Account for Recon",
|
|
owner=self.owner1,
|
|
currency=self.currency,
|
|
group=self.account_group
|
|
)
|
|
account_eur = Account.objects.create(
|
|
name="EUR Account for Recon",
|
|
owner=self.owner1,
|
|
currency=self.eur,
|
|
group=self.account_group
|
|
)
|
|
|
|
# Initial transactions
|
|
Transaction.objects.create(account=account_usd, type=Transaction.Type.INCOME, amount=Decimal('100.00'), date=timezone.localdate(timezone.now()), description='Initial USD', category=self.reconciliation_category, owner=self.owner1, is_paid=True)
|
|
Transaction.objects.create(account=account_eur, type=Transaction.Type.INCOME, amount=Decimal('200.00'), date=timezone.localdate(timezone.now()), description='Initial EUR', category=self.reconciliation_category, owner=self.owner1, is_paid=True)
|
|
Transaction.objects.create(account=account_eur, type=Transaction.Type.EXPENSE, amount=Decimal('50.00'), date=timezone.localdate(timezone.now()), description='EUR Expense', category=self.reconciliation_category, owner=self.owner1, is_paid=True)
|
|
|
|
initial_usd_balance = get_balance(account_usd) # Should be 100.00
|
|
initial_eur_balance = get_balance(account_eur) # Should be 150.00
|
|
self.assertEqual(initial_usd_balance, Decimal('100.00'))
|
|
self.assertEqual(initial_eur_balance, Decimal('150.00'))
|
|
|
|
initial_transaction_count = Transaction.objects.filter(owner=self.owner1).count() # Should be 3
|
|
|
|
formset_data = {
|
|
'form-TOTAL_FORMS': '2',
|
|
'form-INITIAL_FORMS': '2', # Based on view logic, it builds initial data for all accounts
|
|
'form-MAX_NUM_FORMS': '', # Can be empty or a number >= TOTAL_FORMS
|
|
'form-0-account_id': account_usd.id,
|
|
'form-0-new_balance': '120.00', # New balance for USD account (implies +20 adjustment)
|
|
'form-0-category': self.reconciliation_category.id,
|
|
'form-1-account_id': account_eur.id,
|
|
'form-1-new_balance': '150.00', # Same as current balance for EUR account (no adjustment)
|
|
'form-1-category': self.reconciliation_category.id,
|
|
}
|
|
|
|
response = self.client.post(
|
|
reverse('accounts:account_reconciliation'),
|
|
data=formset_data,
|
|
HTTP_HX_REQUEST='true' # Required if view uses @only_htmx
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 204, response.content.decode()) # 204 No Content for successful HTMX POST
|
|
|
|
# Check that only one new transaction was created
|
|
self.assertEqual(Transaction.objects.filter(owner=self.owner1).count(), initial_transaction_count + 1)
|
|
|
|
# Get the newly created transaction
|
|
new_transaction = Transaction.objects.filter(
|
|
account=account_usd,
|
|
description="Balance reconciliation"
|
|
).first()
|
|
|
|
self.assertIsNotNone(new_transaction)
|
|
self.assertEqual(new_transaction.type, Transaction.Type.INCOME)
|
|
self.assertEqual(new_transaction.amount, Decimal('20.00'))
|
|
self.assertEqual(new_transaction.category, self.reconciliation_category)
|
|
self.assertEqual(new_transaction.owner, self.owner1)
|
|
self.assertTrue(new_transaction.is_paid)
|
|
self.assertEqual(new_transaction.date, timezone.localdate(timezone.now()))
|
|
|
|
|
|
# Verify final balances
|
|
self.assertEqual(get_balance(account_usd), Decimal('120.00'))
|
|
self.assertEqual(get_balance(account_eur), Decimal('150.00'))
|