from caching.base import CachingMixin, CachingManager, cached_method
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.utils import timezone
from fiscalyears.fiscalyears import get_start_of_current_fiscal_year
from .managers import TransactionManager
[docs]class BaseJournalEntry(CachingMixin, models.Model):
"""
Journal Entries group :class:`Transactions<entries.models.Transaction>` by
discrete points in time.
For example, a set of transfers, a check being deposited, stipends being
paid.
Journal Entries ensure that :class:`Transactions<Transaction>` are
balanced, that there is an equal amount of credits for every debit.
.. note::
This class is an abstract class to prevent multi-table inheritance.
.. seealso::
Module :mod:`entries.views`
Views related to showing, creating and editing Entries.
.. attribute:: date
The date the entry occured.
.. attribute:: memo
A short description of the Entry.
.. attribute:: comments
Any additional comments about the Entry.
.. attribute:: created_at
The date and time the Entry was created.
.. attribute:: updated_at
The date and time the Entry was last updated. Defaults to
:attr:`created_at`.
"""
date = models.DateField(db_index=True)
created_at = models.DateTimeField(auto_now_add=True, default=timezone.now)
updated_at = models.DateTimeField(auto_now=True, default=timezone.now)
memo = models.CharField(max_length=60)
comments = models.TextField(blank=True, null=True)
objects = CachingManager()
class Meta:
abstract = True
ordering = ['date', 'id']
verbose_name_plural = "journal entries"
def __unicode__(self):
return self.memo
[docs] def get_absolute_url(self):
"""Return a link to the Entry's detail page."""
return reverse('entries.views.show_journal_entry',
args=[str(self.id)])
[docs] def get_edit_url(self):
"""Return an edit link to the Entry's edit page."""
return reverse('entries.views.add_journal_entry', args=[str(self.id)])
[docs] def get_number(self):
"""Return the number of the Entry."""
return "GJ#{0:06d}".format(self.id)
[docs] def in_fiscal_year(self):
"""
Determines whether the :attr:`BaseJournalEntry.date` is in the
current :class:`~fiscalyears.models.FiscalYear`.
Returns True if there is no current
:class:`~fiscalyears.models.FiscalYear`.
:returns: Whether or not the :attr:`date` is in the current
:class:`~fiscalyears.models.FiscalYear`.
:rtype: bool
"""
current_year_start = get_start_of_current_fiscal_year()
if current_year_start is not None and current_year_start > self.date:
return False
return True
[docs]class JournalEntry(BaseJournalEntry):
"""A concrete class of the :class:`BaseJournalEntry` model."""
objects = CachingManager()
[docs] def save(self, *args, **kwargs):
"""Save all related :class:`Transactions<Transaction>` after saving."""
self.full_clean()
super(JournalEntry, self).save(*args, **kwargs)
for transaction in self.transaction_set.all():
transaction.save()
[docs]class BankSpendingEntry(BaseJournalEntry):
"""
Holds information about a Check or ACH payment for a Bank
:class:`~accounts.models.Account`. The :attr:`main_transaction` is linked
to the Bank :class:`~accounts.models.Account`.
Bank Spending Entries credit the Bank :class:`~accounts.models.Account`
(via the :attr:`main_transaction`) while debiting all related
:class:`Transactions<Transaction>`.
.. attribute:: check_number
The number of the Check, if applicable. An ACH Payment should have
no :attr:`check_number` and :attr:`ach_payment` value of ``True`` will
cause the :attr:`check_number` to be set to ``None``. This value must
be unique with respect to the
:attr:`main_transaction's<main_transaction>`
:class:`account<accounts.models.Account>` attribute.
.. attribute:: ach_payment
A boolean representing if this :class:`BankSpendingEntry` is an ACH
Payment or not. If this is ``True`` the :attr:`check_number` will be
set to ``None``.
.. attribute:: payee
An optional Payee for the :class:`BankSpendingEntry`.
.. attribute:: void
A boolean representing whether this :class:`BankSpendingEntry` is void.
If this value switches from ``False`` to ``True``, all of this
:class:`BankSpendingEntry's<BankSpendingEntry>`
:class:`Transactions<Transaction>` will be deleted and it's
:attr:`main_transaction` will have it's
:attr:`~Transaction.balance_delta` set to ``0``. Switching void back to
``False`` will simply allow transactions to be saved again, it will not
recreate any previouse :class:`Transactions<Transaction>`.
.. attribute:: main_transaction
The :class:`Transaction` that links this :class:`BankSpendingEntry`
with it's Bank :class:`~accounts.models.Account`.
"""
# TODO: Change check number to Integer field? Ensure never set to ###ACH###
check_number = models.CharField(max_length=10, blank=True, null=True)
ach_payment = models.BooleanField(default=False,
help_text="Invalidates Check Number")
payee = models.CharField(max_length=50, blank=True, null=True)
void = models.BooleanField(default=False,
help_text="Refunds Associated Transactions.")
main_transaction = models.OneToOneField('Transaction')
objects = CachingManager()
class Meta:
verbose_name_plural = "bank spending entries"
def __unicode__(self):
return self.date.strftime("%d/%m/%y") + " " + self.memo
[docs] def get_absolute_url(self):
"""Return a link to the Entry's detail page."""
return reverse('entries.views.show_bank_entry',
kwargs={'entry_id': str(self.id),
'journal_type': 'CD'})
[docs] def get_edit_url(self):
"""Return the Entry's edit URL."""
return reverse('entries.views.add_bank_entry',
args=['CD', str(self.id)])
[docs] def save(self, *args, **kwargs):
"""Delete related Transactions if void, update Transaction dates."""
# TODO: Should we move this to the base class?
self.full_clean()
if self.void:
[transaction.delete()
for transaction in self.transaction_set.all()]
self.main_transaction.balance_delta = 0
if "VOID" not in self.memo:
self.memo += " VOID"
self.main_transaction.date = self.date
self.main_transaction.save(pull_date=False)
super(BankSpendingEntry, self).save(*args, **kwargs)
for transaction in self.transaction_set.all():
transaction.save()
[docs] def clean(self):
"""
Only a :attr:`check_number` or an :attr:`ach_payment` must be entered,
not both.
The :attr:`check_number` must be unique per
:attr:`BankSpendingEntry.main_transaction`
:attr:`Account<Transaction.account>`.
"""
if not (bool(self.ach_payment) ^ bool(self.check_number)):
raise ValidationError('Either A Check Number or ACH status is '
'required.')
super(BankSpendingEntry, self).clean()
@cached_method
[docs] def get_number(self):
"""Return the formatted :attr:`check_number` or ``##ACH##``."""
if self.ach_payment:
return "##ACH##"
else:
return "CD#{0:06d}".format(int(self.check_number))
[docs]class BankReceivingEntry(BaseJournalEntry):
"""
Holds information about receiving money for a Bank
:class:`~accounts.models.Account`. The :attr:`main_transaction` is linked
to the Bank :class:`~accounts.models.Account`.
Bank Receiving Entries debit the Bank :class:`~accounts.models.Account`
(via the :attr:`main_transaction`) while crediting all related
:class:`Transactions<Transaction>`.
.. attribute:: payor
The Person or Company making the payment.
.. attribute:: main_transaction
The :class:`Transaction` that links this :class:`BankSpendingEntry`
with it's Bank :class:`~accounts.models.Account`.
"""
payor = models.CharField(max_length=50)
main_transaction = models.OneToOneField('Transaction')
objects = CachingManager()
class Meta:
verbose_name_plural = "bank receiving entries"
def __unicode__(self):
return self.memo
[docs] def get_absolute_url(self):
"""Return the Entry's detail page."""
return reverse('entries.views.show_bank_entry',
kwargs={'entry_id': str(self.id),
'journal_type': 'CR'})
[docs] def get_edit_url(self):
"""Return the Entry's edit page."""
return reverse('entries.views.add_bank_entry', args=['CR',
str(self.id)])
[docs] def save(self, *args, **kwargs):
"""Set the date's of all related :class:`Transactions<Transaction>`."""
self.full_clean()
self.main_transaction.date = self.date
self.main_transaction.save(pull_date=False)
super(BankReceivingEntry, self).save(*args, **kwargs)
for transaction in self.transaction_set.all():
transaction.date = self.date
transaction.save()
@cached_method
[docs] def get_number(self):
"""Return the Entry's formatted number."""
return "CR#{0:06d}".format(self.id)
[docs]class Transaction(CachingMixin, models.Model):
"""
Transactions itemize :class:`~accounts.models.Account` balance changes.
Transactions are grouped by Entries and
:class:`Events<events.models.Event>`. Entries group
:class:`Transactions<Transaction>` by date while
:class:`Events<events.models.Event>` group by specific events.
Transactons can be related to Entries through the :attr:`journal_entry`,
:attr:`bankspend_entry` or :attr:`bankreceive_entry` attributes, or through
the ``main_transaction`` attribute of the :class:`BankReceivingEntry` or
:class:`BankSpendingEntry` models. Transactions may only be related to
Entries through one of these ways, never multiple ones.
.. attribute:: journal_entry
The :class:`JournalEntry` this :class:`Transaction` belongs to, if any.
.. attribute:: bankspend_entry
The :class:`BankSpendingEntry` this :class:`Transaction` belongs to, if
any.
.. attribute:: bankreceive_entry
The :class:`BankReceivingEntry` this :class:`Transaction` belongs to,
if any.
.. attribute:: account
The :class:`~accounts.models.Account` this :class:`Transaction` is
charged to.
.. attribute:: detail
Information about the specific charge.
.. attribute:: balance_delta
The change in balance this :class:`Transaction` represents. A positive
value indicates a Credit while a negative value is a Debit.
.. attribute:: event
The :class:`~events.models.Event` this Transaction is related to, if
any.
.. attribute:: reconciled
Whether or not this :class:`Transaction` has been marked as Reconciled.
.. attribute:: date
The :class:`datetime.date` of the :class:`Transaction`. By default,
this is automatically pulled from the related Entry when the
:class:`Transaction` is saved.
"""
journal_entry = models.ForeignKey(JournalEntry, blank=True, null=True)
bankspend_entry = models.ForeignKey(BankSpendingEntry, blank=True,
null=True)
bankreceive_entry = models.ForeignKey(BankReceivingEntry, blank=True,
null=True)
account = models.ForeignKey('accounts.Account', on_delete=models.PROTECT)
detail = models.CharField(max_length=50, help_text="Short description of "
"the charge", blank=True)
balance_delta = models.DecimalField(help_text="Positive balance is a "
"credit, negative is a debit",
max_digits=19, decimal_places=4,
db_index=True)
event = models.ForeignKey('events.Event', blank=True, null=True,
on_delete=models.SET_NULL)
reconciled = models.BooleanField(default=False)
date = models.DateField(blank=True, null=True, db_index=True)
objects = TransactionManager()
class Meta:
ordering = ['date', 'id']
def __unicode__(self):
return self.detail
[docs] def save(self, pull_date=True, *args, **kwargs):
"""Pull the :attr:`date` from the related Entry before saving."""
self.full_clean()
if self.get_journal_entry() and pull_date:
self.date = self.get_journal_entry().date
super(Transaction, self).save(*args, **kwargs)
[docs] def clean(self):
"""Prevent relations to a void :class:`BankSpendingEntry`."""
if self.bankspend_entry and self.bankspend_entry.void:
raise ValidationError("You may not add new Transactions to a void "
"Entry.")
[docs] def get_absolute_url(self):
"""Return a URL to the related Entry's detail page."""
return self.get_journal_entry().get_absolute_url()
[docs] def get_entry_number(self):
"""Return the related Entry's ``number``."""
return self.get_journal_entry().get_number()
[docs] def get_final_account_balance(self):
"""
Return the :class:`Account's<accounts.models.Account>` value balance
after the Transaction has occured.
This is accomplished by subtracting the :attr:`balance_delta` of all
newer :class:`Transactions<Transaction> from the
:class:`Account's<accounts.models.Account>`
:attr:`~accounts.models.Account.balance`.
.. note::
The value balance is not the same as credit/debit balance. For
Assets, Liabilities and Equity Accounts, a debit balance means
a positive value balance instead of the normal negative value
balance.
:returns: The :class:`Account's<accounts.models.Account>`
post-transaction value balance.
:rtype: :class:`~decimal.Decimal`
"""
acct_balance = self.account.balance
# TODO: Refactor query + newer_transactions into manager method
query = (models.Q(date__gt=self.date) |
(models.Q(date=self.date) & models.Q(id__gt=self.id)))
newer_transactions = self.account.transaction_set.filter(
query).aggregate(models.Sum('balance_delta'))
acct_balance -= newer_transactions.get('balance_delta__sum') or 0
if self.account.flip_balance():
acct_balance *= -1
return acct_balance
[docs] def get_initial_account_balance(self):
"""Return the value balance of the :class:`~accounts.models.Account`
from before the Transaction occured.
:returns: The :class:`Account's<accounts.models.Account>`
pre-transaction value balance.
:rtype: :class:`~decimal.Decimal`
"""
final = self.get_final_account_balance()
if self.account.flip_balance():
return final + self.balance_delta
else:
return final - self.balance_delta
[docs] def get_journal_entry(self):
"""Return the related Entry."""
if self.journal_entry:
return self.journal_entry
elif self.bankspend_entry:
return self.bankspend_entry
elif self.bankreceive_entry:
return self.bankreceive_entry
elif (hasattr(self, 'bankreceivingentry') and
self.bankreceivingentry is not None):
return self.bankreceivingentry
elif (hasattr(self, 'bankspendingentry') and
self.bankspendingentry is not None):
return self.bankspendingentry
[docs] def get_memo(self):
"""Return the related Entry's ``memo``."""
return self.get_journal_entry().memo