232 lines
8.8 KiB
Python
232 lines
8.8 KiB
Python
from django.utils.translation import gettext as _
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib import messages
|
|
from .models import Account, JournalEntry, JournalItem
|
|
from .forms import AccountForm, JournalEntryForm
|
|
from django.db.models import Sum, Q
|
|
from django.utils import timezone
|
|
from datetime import datetime
|
|
from django.db import transaction
|
|
import json
|
|
|
|
@login_required
|
|
def accounting_dashboard(request):
|
|
total_assets = sum(acc.balance for acc in Account.objects.filter(account_type='asset'))
|
|
total_liabilities = sum(acc.balance for acc in Account.objects.filter(account_type='liability'))
|
|
total_equity = sum(acc.balance for acc in Account.objects.filter(account_type='equity'))
|
|
|
|
# Revenue and Expenses for current month
|
|
month_start = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
revenue_items = JournalItem.objects.filter(
|
|
account__account_type='income',
|
|
entry__date__gte=month_start
|
|
)
|
|
monthly_revenue = (revenue_items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0) - \
|
|
(revenue_items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0)
|
|
|
|
expense_items = JournalItem.objects.filter(
|
|
account__account_type='expense',
|
|
entry__date__gte=month_start
|
|
)
|
|
monthly_expense = (expense_items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0) - \
|
|
(expense_items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0)
|
|
|
|
context = {
|
|
'total_assets': total_assets,
|
|
'total_liabilities': total_liabilities,
|
|
'total_equity': total_equity,
|
|
'monthly_revenue': monthly_revenue,
|
|
'monthly_expense': monthly_expense,
|
|
'net_profit': monthly_revenue - monthly_expense,
|
|
'recent_entries': JournalEntry.objects.order_by('-date', '-id')[:10]
|
|
}
|
|
return render(request, 'accounting/dashboard.html', context)
|
|
|
|
@login_required
|
|
def chart_of_accounts(request):
|
|
accounts = Account.objects.all().order_by('code')
|
|
return render(request, 'accounting/chart_of_accounts.html', {'accounts': accounts})
|
|
|
|
@login_required
|
|
def account_create_update(request, pk=None):
|
|
if pk:
|
|
account = get_object_or_404(Account, pk=pk)
|
|
else:
|
|
account = None
|
|
|
|
if request.method == 'POST':
|
|
form = AccountForm(request.POST, instance=account)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Account saved successfully."))
|
|
return redirect('chart_of_accounts')
|
|
else:
|
|
form = AccountForm(instance=account)
|
|
|
|
return render(request, 'accounting/account_form.html', {'form': form, 'account': account})
|
|
|
|
@login_required
|
|
def journal_entries(request):
|
|
entries = JournalEntry.objects.all().order_by('-date', '-id')
|
|
return render(request, 'accounting/journal_entries.html', {'entries': entries})
|
|
|
|
@login_required
|
|
def manual_journal_entry(request):
|
|
accounts = Account.objects.filter(is_active=True).order_by('code')
|
|
|
|
if request.method == 'POST':
|
|
form = JournalEntryForm(request.POST)
|
|
# Manual journal entry requires at least two items and they must balance
|
|
account_ids = request.POST.getlist('account[]')
|
|
types = request.POST.getlist('type[]')
|
|
amounts = request.POST.getlist('amount[]')
|
|
|
|
if form.is_valid():
|
|
try:
|
|
with transaction.atomic():
|
|
entry = form.save()
|
|
total_debit = 0
|
|
total_credit = 0
|
|
|
|
for i in range(len(account_ids)):
|
|
acc_id = account_ids[i]
|
|
item_type = types[i]
|
|
amount = float(amounts[i])
|
|
|
|
if amount <= 0: continue
|
|
|
|
JournalItem.objects.create(
|
|
entry=entry,
|
|
account_id=acc_id,
|
|
type=item_type,
|
|
amount=amount
|
|
)
|
|
|
|
if item_type == 'debit':
|
|
total_debit += amount
|
|
else:
|
|
total_credit += amount
|
|
|
|
if round(total_debit, 3) != round(total_credit, 3):
|
|
raise Exception(f"Journal entry does not balance. Total Debit: {total_debit}, Total Credit: {total_credit}")
|
|
|
|
if total_debit == 0:
|
|
raise Exception("Journal entry must have at least one debit and one credit.")
|
|
|
|
messages.success(request, _("Manual journal entry created successfully."))
|
|
return redirect('journal_entries')
|
|
except Exception as e:
|
|
messages.error(request, str(e))
|
|
else:
|
|
form = JournalEntryForm()
|
|
|
|
return render(request, 'accounting/journal_entry_form.html', {
|
|
'form': form,
|
|
'accounts': accounts
|
|
})
|
|
|
|
@login_required
|
|
def account_ledger(request, account_id):
|
|
account = get_object_or_404(Account, id=account_id)
|
|
items = JournalItem.objects.filter(account=account).order_by('entry__date', 'entry__id')
|
|
|
|
# Calculate running balance
|
|
running_balance = 0
|
|
ledger_items = []
|
|
for item in items:
|
|
if account.account_type in ['asset', 'expense']:
|
|
change = item.amount if item.type == 'debit' else -item.amount
|
|
else:
|
|
change = item.amount if item.type == 'credit' else -item.amount
|
|
running_balance += change
|
|
ledger_items.append({
|
|
'item': item,
|
|
'balance': running_balance
|
|
})
|
|
|
|
return render(request, 'accounting/account_ledger.html', {
|
|
'account': account,
|
|
'ledger_items': ledger_items,
|
|
'total_balance': running_balance
|
|
})
|
|
|
|
@login_required
|
|
def trial_balance(request):
|
|
accounts = Account.objects.all().order_by('code')
|
|
trial_data = []
|
|
total_debit = 0
|
|
total_credit = 0
|
|
|
|
for acc in accounts:
|
|
items = acc.journal_items.all()
|
|
debits = items.filter(type='debit').aggregate(total=Sum('amount'))['total'] or 0
|
|
credits = items.filter(type='credit').aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
if debits > 0 or credits > 0:
|
|
trial_data.append({
|
|
'account': acc,
|
|
'debit': debits,
|
|
'credit': credits
|
|
})
|
|
total_debit += debits
|
|
total_credit += credits
|
|
|
|
return render(request, 'accounting/trial_balance.html', {
|
|
'trial_data': trial_data,
|
|
'total_debit': total_debit,
|
|
'total_credit': total_credit
|
|
})
|
|
|
|
@login_required
|
|
def balance_sheet(request):
|
|
assets = Account.objects.filter(account_type='asset')
|
|
liabilities = Account.objects.filter(account_type='liability')
|
|
equity = Account.objects.filter(account_type='equity')
|
|
|
|
# Include Net Income in Equity
|
|
revenue = JournalItem.objects.filter(account__account_type='income').aggregate(
|
|
cr=Sum('amount', filter=Q(type='credit')),
|
|
dr=Sum('amount', filter=Q(type='debit'))
|
|
)
|
|
net_revenue = (revenue['cr'] or 0) - (revenue['dr'] or 0)
|
|
|
|
expenses = JournalItem.objects.filter(account__account_type='expense').aggregate(
|
|
dr=Sum('amount', filter=Q(type='debit')),
|
|
cr=Sum('amount', filter=Q(type='credit'))
|
|
)
|
|
net_expenses = (expenses['dr'] or 0) - (expenses['cr'] or 0)
|
|
net_income = net_revenue - net_expenses
|
|
|
|
asset_total = sum(acc.balance for acc in assets)
|
|
liability_total = sum(acc.balance for acc in liabilities)
|
|
equity_total = sum(acc.balance for acc in equity) + net_income
|
|
|
|
return render(request, 'accounting/balance_sheet.html', {
|
|
'assets': assets,
|
|
'liabilities': liabilities,
|
|
'equity': equity,
|
|
'net_income': net_income,
|
|
'asset_total': asset_total,
|
|
'liability_total': liability_total,
|
|
'equity_total': equity_total,
|
|
'date': timezone.now()
|
|
})
|
|
|
|
@login_required
|
|
def profit_loss(request):
|
|
revenue_accounts = Account.objects.filter(account_type='income')
|
|
expense_accounts = Account.objects.filter(account_type='expense')
|
|
|
|
revenue_total = sum(acc.balance for acc in revenue_accounts)
|
|
expense_total = sum(acc.balance for acc in expense_accounts)
|
|
|
|
return render(request, 'accounting/profit_loss.html', {
|
|
'revenue_accounts': revenue_accounts,
|
|
'expense_accounts': expense_accounts,
|
|
'revenue_total': revenue_total,
|
|
'expense_total': expense_total,
|
|
'net_profit': revenue_total - expense_total,
|
|
'date': timezone.now()
|
|
}) |