import calendar
import datetime
from dateutil import rrule
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.db.models import Sum
from django.http import HttpResponseRedirect
from django.shortcuts import render
from accounts.models import Account, HistoricalAccount
from entries.models import (Transaction, JournalEntry, BankSpendingEntry,
BankReceivingEntry)
from events.models import Event, HistoricalEvent
from .fiscalyears import get_start_of_current_fiscal_year
from .forms import FiscalYearForm, FiscalYearAccountsFormSet
from .models import FiscalYear
@login_required
[docs]def add_fiscal_year(request, template_name="fiscalyears/year_add.html"):
"""
Creates a new :class:`~.models.FiscalYear` using a
:class:`~.forms.FiscalYearForm` and
:data:`~.forms.FiscalYearAccountsFormSet`.
Starting a new :class:`~.models.FiscalYear` involves the following
procedure:
1. Setting a ``period`` and Ending ``month`` and ``year`` for the New
Fiscal Year.
2. Selecting Accounts to exclude from purging of unreconciled
:class:`Transactions<entries.models.Transaction>`.
3. Create a :class:`~accounts.models.HistoricalAccount` for every
:class:`~accounts.models.Account` and month in the previous
:class:`~.models.FiscalYear`, using ending balances for Asset,
Liability and Equity :class:`Accounts<accounts.models.Account>` and
balance changes for the others.
4. Delete all :class:`Journal Entries<entries.models.JournalEntry>`,
except those with unreconciled
:class:`Transactions<entries.models.Transaction>` with
:class:`Accounts<accounts.models.Account>` in the exclude lists.
5. Move the ``balance`` of the ``Current Year Earnings``
:class:`~accounts.models.Account` into the ``Retained Earnings``
:class:`~accounts.models.Account`.
6. Zero the ``balance`` of all Income, Cost of Sales, Expense, Other
Income and Other Expense
:class:`Accounts<accounts.models.Account>`.
:param template_name: The template to use.
:type template_name: string
:returns: HTTP response containing
:class:`~.forms.FiscalYearForm` and
:data:`~.forms.FiscalYearAccountsFormSet` as context.
Redirects if successful POST is sent.
:rtype: HttpResponse or HttpResponseRedirect
"""
previous_year = _get_previous_year_if_exists()
if request.method == 'POST':
fiscal_year_form = FiscalYearForm(request.POST)
accounts_formset = FiscalYearAccountsFormSet(request.POST)
valid = fiscal_year_form.is_valid() and accounts_formset.is_valid()
if valid and previous_year:
start_of_previous_year = get_start_of_current_fiscal_year()
end_of_previous_year = _get_last_day_of_month(previous_year.date)
[_archive_and_delete_event(event) for event in
Event.objects.filter(date__lte=end_of_previous_year)]
months_in_previous_year = _get_months_in_range(
start_of_previous_year, end_of_previous_year)
for month in months_in_previous_year:
last_day_of_month = _get_last_day_of_month(month)
HistoricalAccount.objects.bulk_create(
[_build_historical_account(account, last_day_of_month) for
account in Account.objects.all()]
)
excluded_transactions = _get_excluded_transactions(
accounts_formset)
journals = [Journal.objects.filter(date__lte=end_of_previous_year)
for Journal in (JournalEntry, BankReceivingEntry,
BankSpendingEntry)]
for entries in journals:
for entry in entries:
_delete_entry_if_not_excluded(entry, excluded_transactions)
[_correct_account_balance(account, end_of_previous_year) for
account in Account.objects.all()]
_transfer_current_year_earnings(end_of_previous_year)
fiscal_year_form.save()
messages.success(request, "Your previous fiscal year has been "
"closed and a new fiscal year has been started.")
return HttpResponseRedirect(reverse(
'accounts.views.show_accounts_chart'))
elif valid:
fiscal_year_form.save()
messages.success(request, "A new fiscal year has been started.")
return HttpResponseRedirect(reverse(
'accounts.views.show_accounts_chart'))
else:
fiscal_year_form = FiscalYearForm()
accounts_formset = FiscalYearAccountsFormSet(
queryset=Account.objects.order_by('last_reconciled',
'full_number'))
return render(request, template_name, locals())
def _get_previous_year_if_exists():
"""Return the last FiscalYear or False if none exists."""
try:
previous_year = FiscalYear.objects.latest()
except FiscalYear.DoesNotExist:
previous_year = False
return previous_year
def _archive_and_delete_event(event):
"""Create a HistoricalEvent and delete the Event."""
debit_total, credit_total, net_change = event.transaction_set.get_totals(
net_change=True)
HistoricalEvent.objects.create(
name=event.name, number=event.number, date=event.date, city=event.city,
state=event.state, debit_total=debit_total, credit_total=credit_total,
net_change=net_change)
event.delete()
def _get_months_in_range(start_date, stop_date):
"""Return a list of datetime.date months between the start & stop dates."""
return rrule.rrule(rrule.MONTHLY, dtstart=start_date, until=stop_date)
def _build_historical_account(account, month):
"""Create a HistoricalAccount for the specified account and month."""
if account.type in (1, 2, 3):
amount = account.get_balance_by_date(date=month)
else:
amount = account.get_balance_change_by_month(month)
if account.flip_balance(): # Flip back to credit/debit
amount *= -1 # amount from value amount
historical_account = HistoricalAccount(
account=account, date=month, name=account.name, type=account.type,
number=account.get_full_number(), amount=amount)
return historical_account
def _get_excluded_transactions(accounts_formset):
"""
Process a FiscalYearAccountsFormSet and return a set of excluded
Transactions.
"""
excluded_accounts = [form.instance for form in accounts_formset if
form.cleaned_data.get('exclude')]
excluded_transactions = frozenset(Transaction.objects.filter(
account__in=excluded_accounts, reconciled=False))
return excluded_transactions
def _get_last_day_of_month(month):
"""Return the last day of the specified month."""
last_day_of_month = month.replace(
day=calendar.monthrange(month.year, month.month)[1])
return last_day_of_month
def _get_all_transactions(entry):
"""Get all Transactions of the Entry, including a main_transaction."""
entry_transactions = list(entry.transaction_set.all())
if hasattr(entry, 'main_transaction'):
entry_transactions.append(entry.main_transaction)
return entry_transactions
def _delete_entry_if_not_excluded(entry, excluded_transactions):
"""Delete an Entry if none of it's Transactions are excluded."""
entry_transactions = _get_all_transactions(entry)
entry_is_not_excluded = excluded_transactions.isdisjoint(
entry_transactions)
if entry_is_not_excluded:
[transaction.delete() for transaction in entry_transactions]
entry.delete()
def _correct_account_balance(account, historical_year_end):
"""
Set the Account balance to the latest HistoricalAccount balance plus any
Transactions after the end of the last Fiscal Year.
"""
new_year_sum = account.transaction_set.filter(
date__gt=historical_year_end).aggregate(Sum('balance_delta'))
account.balance = new_year_sum.get('balance_delta__sum') or 0
if account.type in (1, 2, 3):
hist_acct = account.historicalaccount_set.latest()
account.balance += hist_acct.amount
account.save()
def _transfer_current_year_earnings(entry_date):
"""Transfer the Current Year Earnings balance into Retained Earnings."""
current_earnings = Account.objects.get(name='Current Year Earnings')
retained_earnings = Account.objects.get(name='Retained Earnings')
historical_current = current_earnings.historicalaccount_set.latest()
transfer_date = entry_date + datetime.timedelta(days=1)
entry = JournalEntry.objects.create(date=transfer_date,
memo='End of Fiscal Year Adjustment')
Transaction.objects.create(journal_entry=entry, account=current_earnings,
balance_delta=historical_current.amount * -1)
Transaction.objects.create(journal_entry=entry, account=retained_earnings,
balance_delta=historical_current.amount)