38086-vm/accounting/views.py
2026-02-10 17:53:28 +00:00

294 lines
12 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 core.models import Sale, Purchase, Product
from django.db.models import Sum, Q, Value, DecimalField, F
from django.db.models.functions import Coalesce
from django.utils import timezone
from datetime import datetime, date
from django.db import transaction
import json
@login_required
def vat_report(request):
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
if not start_date:
start_date = timezone.now().replace(day=1).strftime('%Y-%m-%d')
if not end_date:
end_date = timezone.now().strftime('%Y-%m-%d')
# Convert strings to date objects for filtering
# Note: We filter by the day inclusive
sales = Sale.objects.filter(created_at__date__gte=start_date, created_at__date__lte=end_date).exclude(status='cancelled')
purchases = Purchase.objects.filter(created_at__date__gte=start_date, created_at__date__lte=end_date).exclude(status='cancelled').prefetch_related('items__product')
# Output VAT (Sales)
total_sales_subtotal = sales.aggregate(sum=Sum('subtotal'))['sum'] or 0
total_output_vat = sales.aggregate(sum=Sum('vat_amount'))['sum'] or 0
total_sales_gross = sales.aggregate(sum=Sum('total_amount'))['sum'] or 0
# Input VAT (Purchases) - Estimated based on Product VAT rate
# Since Purchase model doesn't store VAT explicitly, we calculate it from items
total_purchases_subtotal = 0
total_input_vat = 0
for purchase in purchases:
purchase_vat = 0
purchase_subtotal = 0
for item in purchase.items.all():
# Assume item line_total is cost * quantity
# We calculate VAT on top.
rate = float(item.product.vat)
line_total = float(item.line_total)
tax = line_total * (rate / 100.0)
purchase_vat += tax
purchase_subtotal += line_total
total_input_vat += purchase_vat
total_purchases_subtotal += purchase_subtotal
total_purchases_gross = total_purchases_subtotal + total_input_vat
context = {
'start_date': start_date,
'end_date': end_date,
'total_sales_subtotal': total_sales_subtotal,
'total_output_vat': total_output_vat,
'total_sales_gross': total_sales_gross,
'total_purchases_subtotal': total_purchases_subtotal,
'total_input_vat': total_input_vat,
'total_purchases_gross': total_purchases_gross,
'net_vat': float(total_output_vat) - total_input_vat,
'currency': 'OMR',
}
return render(request, 'accounting/vat_report.html', context)
@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.annotate(
total_debit=Coalesce(Sum('items__amount', filter=Q(items__type='debit')), Value(0), output_field=DecimalField()),
total_credit=Coalesce(Sum('items__amount', filter=Q(items__type='credit')), Value(0), output_field=DecimalField())
).prefetch_related('items__account').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()
})