Source code for entries.forms
from decimal import Decimal
from django import forms
from django.forms.formsets import formset_factory
from django.forms.models import inlineformset_factory
from parsley.decorators import parsleyfy
from accounts.models import Account
from core.core import today_in_american_format, remove_trailing_zeroes
from core.forms import RequiredBaseFormSet, RequiredBaseInlineFormSet
from events.models import Event # Needed for Sphinx
from fiscalyears.fiscalyears import get_start_of_current_fiscal_year
from .models import (Transaction, JournalEntry, BankSpendingEntry,
BankReceivingEntry)
[docs]class BaseEntryForm(object):
"""An abstraction of General and Bank Entries."""
def __init__(self, *args, **kwargs):
"""Initialize the date to today and put it in MM/DD/YYYY format."""
super(BaseEntryForm, self).__init__(*args, **kwargs)
self._initialize_date()
def _initialize_date(self):
"""Default to today, formatted as MM/DD/YYYY."""
if hasattr(self, 'instance') and self.instance.pk:
formatted_date = self.instance.date.strftime('%m/%d/%Y')
self.initial['date'] = formatted_date
else:
self.initial['date'] = today_in_american_format()
[docs] def clean_date(self):
"""The date must be in the Current :class:`FiscalYear`."""
input_date = self.cleaned_data.get('date')
fiscal_year_start = get_start_of_current_fiscal_year()
if fiscal_year_start is not None and input_date < fiscal_year_start:
raise forms.ValidationError("The date must be in the current "
"Fiscal Year.")
return input_date
@parsleyfy
[docs]class JournalEntryForm(BaseEntryForm, forms.ModelForm):
"""A form for :class:`JournalEntries<.models.JournalEntry>`."""
class Meta:
model = JournalEntry
widgets = {
'date': forms.DateInput(attrs={'data-americandate': True,
'class': 'form-control'}),
'comments': forms.Textarea(attrs={'rows': 2, 'cols': 50,
'class': 'form-control'}),
'memo': forms.TextInput(attrs={'class': 'form-control'})
}
@parsleyfy
[docs]class TransactionForm(forms.ModelForm):
"""A form for :class:`Transactions<.models.Transaction>`.
It splits the :attr:`~.models.Transaction.balance_delta` field into a
``credit`` and ``debit`` field. A debit results in a negative
:attr:`~.models.Transaction.balance_delta` and a credit results in a
positive :attr:`~.models.Transaction.balance_delta`.
"""
credit = forms.DecimalField(
required=False, min_value=Decimal("0.01"),
widget=forms.TextInput(
attrs={'size': 10, 'maxlength': 10,
'class': 'credit form-control enter-mod'})
)
debit = forms.DecimalField(
required=False, min_value=Decimal("0.01"),
widget=forms.TextInput(
attrs={'size': 10, 'maxlength': 10,
'class': 'debit form-control enter-mod'})
)
class Meta:
model = Transaction
fields = ('account', 'detail', 'debit', 'credit', 'event',)
widgets = {
'account': forms.Select(
attrs={'class': 'account account-autocomplete '
'form-control enter-mod'}),
'detail': forms.TextInput(
attrs={'class': 'form-control enter-mod'}),
'event': forms.Select(
attrs={'class': 'form-control enter-mod'}),
}
def __init__(self, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
_set_minimal_queryset_for_account(self, 'account')
self._assign_balance_delta_to_debit_or_credit_field()
[docs] def clean(self):
"""
Make sure only a credit or debit is entered and set it to the
balance_delta.
"""
super(TransactionForm, self).clean()
if any(self.errors) or self.cleaned_data.get('DELETE', False):
return self.cleaned_data
cleaned_data = self.cleaned_data
debit = cleaned_data.get('debit')
credit = cleaned_data.get('credit')
if credit and debit:
raise forms.ValidationError("Please enter only one debit or "
"credit per line.")
elif credit:
cleaned_data['balance_delta'] = credit
elif debit:
cleaned_data['balance_delta'] = -1 * debit
else:
raise forms.ValidationError("Either a credit or a debit is "
"required")
return cleaned_data
def _assign_balance_delta_to_debit_or_credit_field(self):
"""
If the form's :class:`~.models.Transaction` instance has a
`balance_delta` set either the `credit` or `debit` field to it's
absolute value, depending on if the balance_delta is positive or
negative, respectively.
"""
balance_delta = self.instance.balance_delta
if balance_delta is not None:
if balance_delta < 0:
self.initial['debit'] = remove_trailing_zeroes(
abs(balance_delta)
)
else:
self.initial['credit'] = remove_trailing_zeroes(balance_delta)
def _set_minimal_queryset_for_account(form, field_name):
"""Show Only a Pre-Existing Selection for a ModelChoiceField.
This replaces the `queryset` of the `field_name` on the `form` with a
queryset containing only it's instance's Account or an empty queryset -
depending on if `form` has an instance or not.
This is used to limit the size of HTML when the options for `field_name`
are loaded by AJAX.
"""
if form.is_bound:
return
if hasattr(form, 'instance') and form.instance.account_id is not None:
form.fields[field_name].queryset = Account.objects.filter(
id=form.instance.account_id)
else:
form.fields[field_name].queryset = Account.objects.none()
[docs]class BaseTransactionFormSet(RequiredBaseInlineFormSet):
"""
A FormSet that validates that a set of
:class:`Transactions<.models.Transaction>` is balanced.
"""
[docs] def clean(self):
"""Checks that debits and credits balance out."""
super(BaseTransactionFormSet, self).clean()
if any(self.errors):
return
balance = Decimal(0)
for form in self.forms:
if form.cleaned_data.get('DELETE'):
continue
balance += form.cleaned_data.get('balance_delta', 0)
if balance != 0:
raise forms.ValidationError("The total amount of Credits must be "
"equal to the total amount of Debits.")
return
#: A FormSet for :class:`Transactions<.models.Transaction>`, derived from the
#: :class:`BaseTransactionFormSet`.
TransactionFormSet = inlineformset_factory(JournalEntry, Transaction,
extra=20,
form=TransactionForm,
formset=BaseTransactionFormSet,
can_delete=True)
@parsleyfy
[docs]class TransferForm(BaseEntryForm, forms.Form):
"""
A form for Transfer Entries, a specialized :class:`~.models.JournalEntry`.
Transfer Entries move a discrete :attr:`amount` between two
:class:`Accounts<accounts.models.Account>`. The :attr:`source` is debited
while the :attr:`destination` is credited.
.. attribute:: source
The :class:`~accounts.models.Account` to remove the amount from.
.. attribute:: destination
The :class:`~accounts.models.Account` to move the amount to.
.. attribute:: amount
The amount of currency to transfer between
:class:`Accounts<accounts.models.Account>`.
.. attribute:: detail
Any additional details about the charge.
"""
source = forms.ModelChoiceField(
queryset=Account.objects.active().order_by('name'),
widget=forms.Select(attrs={'class': 'source account-autocomplete '
'form-control enter-mod'})
)
destination = forms.ModelChoiceField(
queryset=Account.objects.active().order_by('name'),
widget=forms.Select(
attrs={'class': 'destination account-autocomplete '
'form-control enter-mod'})
)
# TODO: This is repeated in BaseBankForm & AccountReconcileForm and ugly
amount = forms.DecimalField(
max_digits=19, decimal_places=4, min_value=Decimal("0.01"),
widget=forms.TextInput(
attrs={'size': 10, 'maxlength': 10,
'class': 'amount form-control enter-mod'})
)
detail = forms.CharField(
max_length=50, required=False,
widget=forms.TextInput(attrs={'class': 'form-control enter-mod'})
)
def __init__(self, *args, **kwargs):
super(TransferForm, self).__init__(*args, **kwargs)
_set_minimal_queryset_for_account(self, 'source')
_set_minimal_queryset_for_account(self, 'destination')
[docs] def clean(self):
"""
Ensure that the source and destination
:class:`Accounts<accounts.models.Account>` are not the same.
"""
super(TransferForm, self).clean()
if any(self.errors):
return self.cleaned_data
cleaned_data = self.cleaned_data
if cleaned_data.get('source') == cleaned_data.get('destination'):
raise forms.ValidationError("The Source and Destination Accounts "
"must be different.")
return cleaned_data
#: A FormSet for Transfer Transactions, derived from the :class:`TransferForm`.
TransferFormSet = formset_factory(TransferForm, extra=20, can_delete=True,
formset=RequiredBaseFormSet)
[docs]class BaseBankForm(BaseEntryForm, forms.ModelForm):
"""
A Base form for common elements between the :class:`BankSpendingForm` and
the :class:`BankReceivingForm`.
The ``account`` and ``amount`` fields be used to create the Entries
``main_transaction``.
.. attribute:: account
The Bank :class:`~accounts.models.Account` the Entry is for.
.. attribute:: amount
The total amount this Entry represents.
"""
account = forms.ModelChoiceField(
queryset=Account.objects.get_banks(),
widget=forms.Select(attrs={'class': 'form-control'})
)
amount = forms.DecimalField(
min_value=Decimal(".01"),
widget=forms.TextInput(attrs={'size': 10, 'maxlength': 10,
'id': 'entry_amount',
'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
"""
Pull the initial ``account`` and ``amount`` fields from the
``main_transaction``.
"""
super(BaseBankForm, self).__init__(*args, **kwargs)
self._initialize_account_and_amount_from_main_transaction()
def _initialize_account_and_amount_from_main_transaction(self):
"""Use the main_transaction's account and amount."""
if hasattr(self.instance, 'main_transaction'):
main_transaction = self.instance.main_transaction
if main_transaction is not None:
self.initial['account'] = main_transaction.account
self.initial['amount'] = remove_trailing_zeroes(
abs(main_transaction.balance_delta)
)
[docs] def clean(self):
"""
Cleaning the :class:`BaseBankForm` will modify or create the
:attr:`BankSpendingEntry.main_transaction` or
:attr:`BankReceivingEntry.main_transaction`.
The ``main_transaction`` will not be saved until this form's save
method is called.
"""
super(BaseBankForm, self).clean()
if any(self.errors):
return self.cleaned_data
cleaned_data = self._update_or_add_main_transaction(self.cleaned_data)
return cleaned_data
def _update_or_add_main_transaction(self, cleaned_data):
"""
Update the form's main_transaction, instantiating one if it does not
exist.
"""
account = cleaned_data.get('account')
amount = cleaned_data.get('amount')
memo = cleaned_data.get('memo')
date = cleaned_data.get('date')
try:
self.instance.main_transaction.account = account
self.instance.main_transaction.balance_delta = amount
self.instance.main_transaction.detail = memo
self.instance.main_transaction.date = date
cleaned_data['main_transaction'] = self.instance.main_transaction
except Transaction.DoesNotExist:
main_transaction = Transaction(account=account, detail=memo,
balance_delta=amount, date=date)
cleaned_data['main_transaction'] = main_transaction
self.instance.main_transaction = main_transaction
return cleaned_data
[docs] def save(self, *args, **kwargs):
"""
Saving the :class:`BaseBankFormForm` will save both the
:class:`BaseBankForm` and the
:attr:`BankSpendingEntry.main_transaction` or
:attr:`BankReceivingEntry.main_transaction`.
"""
self.cleaned_data.get('main_transaction').save()
self.instance.main_transaction = self.cleaned_data['main_transaction']
super(BaseBankForm, self).save(*args, **kwargs)
@parsleyfy
[docs]class BankSpendingForm(BaseBankForm):
"""A form for the :class:`~.models.BankSpendingEntry` model."""
class Meta:
model = BankSpendingEntry
fields = ('account', 'date', 'ach_payment', 'check_number', 'payee',
'memo', 'amount', 'comments')
# TODO: Move to basebankform?
widgets = {
'date': forms.DateInput(attrs={'data-americandate': True,
'class': 'form-control'}),
'comments': forms.Textarea(attrs={'rows': 2, 'cols': 50,
'class': 'form-control'}),
'ach_payment': forms.CheckboxInput(attrs={'class':
'form-control'}),
'check_number': forms.TextInput(attrs={'class': 'form-control'}),
'memo': forms.TextInput(attrs={'class': 'form-control'}),
'payee': forms.TextInput(attrs={'class': 'form-control'}),
}
@parsleyfy
[docs]class BankReceivingForm(BaseBankForm):
"""A form for the :class:`~.models.BankReceivingEntry` model."""
class Meta:
model = BankReceivingEntry
fields = ('account', 'date', 'payor', 'amount', 'memo', 'comments')
widgets = {
'date': forms.DateInput(attrs={'data-americandate': True,
'class': 'form-control'}),
'comments': forms.Textarea(attrs={'rows': 2, 'cols': 50,
'class': 'form-control'}),
'memo': forms.TextInput(attrs={'class': 'form-control'}),
'payor': forms.TextInput(attrs={'class': 'form-control'}),
}
[docs] def clean_amount(self):
"""Negate the ``amount`` field.
:class:`BankReceivingEntries<.models.BankReceivingEntry>` debit their
bank :class:`~accounts.models.Account>` so a ``positive`` amount should
become a ``debit`` and vice versa.
"""
amount = self.cleaned_data.get('amount')
return -1 * amount
@parsleyfy
[docs]class BankTransactionForm(forms.ModelForm):
"""A form for entry of Bank :class:`Transactions<.models.Transaction>`.
Bank :class:`Transactions<.models.Transaction>` do not have a ``credit``
and ``debit`` field, instead they have only an ``amount``. Whether this
amount is a ``credit`` or ``debit`` depends on if the
:class:`~.models.Transaction` is related to a
:class:`.models.BankSpendingEntry` or a :class:`.models.BankSpendingEntry`.
"""
amount = forms.DecimalField(
max_digits=19, decimal_places=4, min_value=Decimal("0.01"),
widget=forms.TextInput(
attrs={'size': 10, 'maxlength': 10,
'class': 'amount form-control enter-mod'})
)
class Meta:
model = Transaction
fields = ('account', 'detail', 'amount', 'event',)
widgets = {
'account': forms.Select(
attrs={'class': 'account account-autocomplete '
'form-control enter-mod'}),
'detail': forms.TextInput(
attrs={'class': 'form-control enter-mod'}),
'event': forms.Select(
attrs={'class': 'form-control enter-mod'}),
}
def __init__(self, *args, **kwargs):
super(BankTransactionForm, self).__init__(*args, **kwargs)
_set_minimal_queryset_for_account(self, 'account')
self._assign_balance_delta_to_amount_field()
[docs] def clean(self):
"""Set the ``balance_delta`` to the entered ``amount``."""
super(BankTransactionForm, self).clean()
if any(self.errors):
return self.cleaned_data
cleaned_data = self.cleaned_data
# TODO: Can we use balance_delta instead of also creating an amount?
cleaned_data['balance_delta'] = cleaned_data.get('amount')
return cleaned_data
def _assign_balance_delta_to_amount_field(self):
"""
If the instance has a balance_delta, set the amount field to it's
absolute value.
"""
balance_delta = self.instance.balance_delta
if balance_delta is not None:
self.initial['amount'] = remove_trailing_zeroes(
abs(balance_delta)
)
[docs]class BaseBankTransactionFormSet(forms.models.BaseInlineFormSet):
"""An InlineFormSet used to validate it's form's amounts balance with the
``self.entry_form``'s amount.
.. note::
The ``self.entry_form`` attribute must be set by the view instantiating
this form.
"""
[docs] def clean(self):
"""Checks that the Transactions' amounts balance the Entry's amount."""
super(BaseBankTransactionFormSet, self).clean()
if any(self.errors):
return getattr(self, 'cleaned_data', None)
balance = abs(self.entry_form.cleaned_data.get('amount'))
for form in self.forms:
if form.cleaned_data.get('DELETE'):
continue
balance -= abs(form.cleaned_data.get('amount', 0))
if balance != 0:
raise forms.ValidationError("The Entry Amount must equal the "
"total Transaction Amount.")
#: A FormSet of :class:`Transactions<.models.Transaction>` for use with
#: :class:`BankSpendingEntries<.models.BankSpendingEntry>`, derived from the
#: :class:`BaseBankTransactionFormSet`.
BankSpendingTransactionFormSet = inlineformset_factory(
BankSpendingEntry, Transaction, form=BankTransactionForm,
formset=BaseBankTransactionFormSet, extra=5, can_delete=True)
#: A FormSet of :class:`Transactions<.models.Transaction>` for use with
#: :class:`BankReceivingEntries<.models.BankReceivingEntry>`, derived from the
#: :class:`BaseBankTransactionFormSet`.
BankReceivingTransactionFormSet = inlineformset_factory(
BankReceivingEntry, Transaction, form=BankTransactionForm,
formset=BaseBankTransactionFormSet, extra=5, can_delete=True)