expenses_manager/expenses_manager/expenses/views.py

924 lines
25 KiB
Python

from operator import truediv
from datetime import date, datetime
from django.contrib import messages
from django.template import context
from .models import Account, Category, Expense, FuelEntry, Tag, Income
from .forms import ExpenseForm, IncomeForm, TagForm, AccountForm, FuelEntryForm, CategoryForm
# from dateutli.relativedelta import relativedelta
from django.db.models import Sum
from django.contrib.auth import login
from django.core.paginator import Paginator
from django.utils.ipv6 import is_valid_ipv6_address
from django.db.models.functions import ExtractMonth, ExtractYear
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render, redirect
MONTHS = {
1:'ENERO',
2:'FEBRERO',
3:'MARZO',
4:'ABRIL',
5:'MAYO',
6:'JUNIO',
7:'JULIO',
8:'AGOSTO',
9:'SEPTIEMBRE',
10:'OCTUBRE',
11:'NOVIEMBRE',
12:'DICIEMBRE'
}
def _get_int(value):
try:
return int(value)
except (TypeError, ValueError):
return None
def sub_months(year, month, n):
month -= n
while month <= 0:
month += 12
year -= 1
return year, month
@login_required
def home(request):
expenses = Expense.objects.filter(owner=request.user)
# Last expenses
last_expenses = (
expenses
.select_related('category')
.order_by('-date')[:5]
)
# Simple KPIs (current month)
today = date.today()
month_expenses = expenses.filter(
date__year=today.year,
date__month=today.month
)
kpi_total = month_expenses.aggregate(
total=Sum('amount')
)['total'] or 0
kpi_count = month_expenses.count()
kpi_categories = (
month_expenses
.values('category')
.distinct()
.count()
)
six_months = []
for i in range(5, -1, -1):
y, m = sub_months(today.year, today.month, i)
six_months.append((y, m))
mini_data = []
for y, m in six_months:
total = expenses.filter(
date__year=y,
date__month=m
).aggregate(total=Sum('amount'))['total'] or 0
mini_data.append({
'label': f'{m}/{y}',
'total': float(total),
})
return render(request, 'expenses/home.html', {
'active_menu': 'home',
'last_expenses': last_expenses,
'kpi_total': kpi_total,
'kpi_count': kpi_count,
'kpi_categories': kpi_categories,
'mini_chart_labels': [x['label'] for x in mini_data],
'mini_chart_data': [x['total'] for x in mini_data],
})
@login_required
def expense_list(request):
expenses = Expense.objects.filter(owner=request.user)
categories = Category.objects.filter(owner=request.user)
year_list = (
Expense.objects.filter(owner=request.user)
.dates('date', 'year')
)
months = list(range(1, 13))
# Filters
year = _get_int(request.GET.get('year'))
month = _get_int(request.GET.get('month'))
category = _get_int(request.GET.get('category'))
account_id = _get_int(request.GET.get('account'))
tag_ids = request.GET.getlist('tag')
tag_ids = [int(t) for t in tag_ids]
if year:
expenses = expenses.filter(date__year=year)
if month:
expenses = expenses.filter(date__month=month)
if category:
expenses = expenses.filter(category_id=category)
if tag_ids:
expenses = expenses.filter(tags__id__in=tag_ids).distinct()
if account_id:
expenses = expenses.filter(account_id=account_id)
selected_tags = tag_ids or []
expenses = expenses.order_by('-date')
total_amount = expenses.aggregate(
total=Sum('amount')
)['total'] or 0
expense_count = expenses.count()
category_count = (
expenses.values('category')
.distinct()
.count()
)
# Pagination
paginator = Paginator(expenses, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
query_params = request.GET.copy()
query_params.pop('page', None)
# tags with state
tags_with_state = []
for tag in Tag.objects.filter(owner=request.user):
if tag.id in selected_tags:
new_tags = [t for t in selected_tags if t != tag.id]
active = True
else:
new_tags = selected_tags + [tag.id]
active = False
query = '&'.join([f'tag={t}' for t in new_tags])
tags_with_state.append({
'id': tag.id,
'name': tag.name,
'active': active,
'query': query,
})
advanced_filters_open = bool(
category or selected_tags
)
return render (
request,
'expenses/expense_list.html',
{
'active_menu': 'expenses',
'expenses': page_obj,
'page_obj': page_obj,
'selected_year': year,
'selected_month': month,
'selected_category': category,
'categories': categories,
'year_list': [y.year for y in year_list],
'months': months,
'kpi_total': total_amount,
'kpi_count': expense_count,
'kpi_categories': category_count,
'selected_tags': selected_tags,
'tags': Tag.objects.filter(owner=request.user),
'tags_with_state': tags_with_state,
'accounts': Account.objects.filter(owner=request.user),
'selected_account': account_id,
'advanced_filters_open' : advanced_filters_open,
'query_params': query_params.urlencode(),
},
)
@login_required
def expense_create(request):
if request.method == "POST":
form = ExpenseForm(request.POST, user=request.user)
if form.is_valid():
expense = form.save(commit=False)
expense.owner = request.user
expense.save()
form.save_m2m()
messages.success(request, 'Gasto creado correctamente.')
return redirect('expense_list')
else:
form = ExpenseForm(user=request.user)
return render(
request,
'expenses/expense_form.html',
{'active_menu': 'expenses','form': form},
)
@login_required
def expense_edit(request, pk):
# sourcery skip: assign-if-exp, merge-else-if-into-elif
expense = get_object_or_404(
Expense,
pk=pk,
owner=request.user,
)
if request.method == "POST":
form = ExpenseForm(
request.POST or None,
instance=expense,
user=request.user)
if form.is_valid():
expense = form.save()
messages.success(request, 'Gasto actualizado')
return redirect('expense_list')
else:
form = ExpenseForm(instance=expense, user=request.user)
return render(
request,
'expenses/expense_form.html', {
'active_menu': 'expenses',
'form': form,
}
)
@login_required
def expense_delete(request, pk):
expense = get_object_or_404(
Expense,
pk=pk,
owner=request.user,
)
if request.method == 'POST':
expense.delete()
messages.success(request, 'Gasto eliminado correctamente.')
return redirect('expense_list')
return render(
request,
'expenses/expense_confirm_delete.html',
{'active_menu': 'expenses','expense': expense},
)
@login_required
def dashboard(request):
# ------------------
# Filters
# ------------------
year = _get_int(request.GET.get('year'))
month = _get_int(request.GET.get('month'))
period = request.GET.get('period')
current_year = date.today().year
account_id = _get_int(request.GET.get('account'))
accounts = Account.objects.filter(
owner=request.user,
active=True,
)
selected_year = year or current_year
selected_month = month
compare_enabled = request.GET.get("compare") == "1"
# ------------------
# Queryset base
# -----------------
expenses = Expense.objects.filter(owner=request.user)
selected_account_obj = None
kpi_balance = None
if account_id:
expenses = expenses.filter(account_id=account_id)
selected_account_obj = accounts.filter(id=account_id).first()
kpi_balance = selected_account_obj.current_balance() if selected_account_obj else 0
else:
kpi_balance = sum(
account.current_balance() for account in accounts
)
today = date.today()
if period == 'this_month':
selected_year = today.year
selected_month = today.month
elif period == 'last_month':
if today.month == 1:
selected_year = today.year - 1
selected_month = 12
else:
selected_year = today.year
selected_month = today.month
elif period == 'this_year':
selected_year = today.year
selected_month = None
expenses_filtered = expenses.filter(date__year=selected_year)
if selected_month:
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
total_amount = expenses_filtered.aggregate(
total=Sum('amount')
)['total'] or 0
expense_count = expenses_filtered.count()
category_count = (
expenses_filtered
.values('category')
.distinct()
.count()
)
# ------------------
# Totals by category
# -----------------
by_category = (
expenses_filtered
.values('category__name')
.annotate(total=Sum('amount'))
.order_by('category__name')
)
# ------------------
# Totals by month
# -----------------
by_month_qs = (
expenses_filtered
.annotate(month=ExtractMonth('date'))
.values('month')
.annotate(total=Sum('amount'))
)
month_totals = {
row['month']: float(row['total'])
for row in by_month_qs
}
months = list(range(1, 13))
by_month = [
{
'month': m,
'total': month_totals.get(m, 0),
}
for m in months
]
# ------------------
# Availables years
# -----------------
year_list = (
expenses
.annotate(year=ExtractYear('date'))
.values_list('year', flat=True)
.distinct()
.order_by('year')
)
# ------------------
# Chart
# -----------------
chart_labels = months
chart_totals = [row['total'] for row in by_month]
# ------------------
# Compare period
# -----------------
previous_total = None
kpi_difference = None
percentage = None
category_comparison = None
kpi_trend = None
kpi_difference_abs = None
if compare_enabled:
previous_expenses = Expense.objects.filter(owner=request.user)
if account_id:
previous_expenses.filter(account_id=account_id)
if selected_month:
# Monthly compare
if selected_month == 1:
prev_year = selected_year - 1
prev_month = 12
else:
prev_year = selected_year
prev_month = selected_month - 1
previous_expenses = previous_expenses.filter(
date__year=prev_year,
date__month=prev_month
)
else:
# Anual compare
prev_year = selected_year - 1
previous_expenses = previous_expenses.filter(
date__year=prev_year
)
previous_total = (
previous_expenses.aggregate(total=Sum('amount'))['total'] or 0
)
kpi_difference = total_amount - previous_total
if previous_total:
percentage = (kpi_difference / previous_total) * 100
kpi_trend = None
if kpi_difference is not None:
if kpi_difference > 0:
kpi_trend = 'up'
elif kpi_difference < 0:
kpi_trend = 'down'
else:
kpi_trend = 'equal'
kpi_difference_abs = abs(kpi_difference) if kpi_difference is not None else None
# ------------------
# Previous expenses by category
# ------------------
previous_by_category = (
previous_expenses
.values('category__name')
.annotate(total=Sum('amount'))
)
current_map = {
row['category__name']: row['total']
for row in by_category
}
previous_map = {
row['category__name']: row['total']
for row in previous_by_category
}
all_categories = set(current_map.keys()) | set(previous_map.keys())
category_comparison = []
for category in all_categories:
current_total = current_map.get(category, 0)
previous_total_cat = previous_map.get(category, 0)
difference = current_total - previous_total_cat
category_comparison.append({
'category': category,
'current': current_total,
'previous': previous_total_cat,
'difference': difference,
'difference_abs': abs(difference),
})
# ------------------
# Previous expenses by category
# ------------------
accounts_charts = []
for account in accounts:
monthly_data = account.monthly_balance(selected_year)
m_balance = [row['balance'] for row in monthly_data]
accounts_charts.append({
'id': account.id,
'name': account.name,
'data': m_balance,
'current_balance': account.current_balance(),
})
# Send the data to the dashboard
return render(request, 'expenses/dashboard.html', {
'active_menu': 'dashboard',
'by_category': by_category,
'by_month': by_month,
'chart_labels': chart_labels,
'chart_data': chart_totals,
'year_list': year_list,
'months': months,
'selected_year': selected_year,
'selected_month': selected_month,
'kpi_total': total_amount,
'kpi_count': expense_count,
'kpi_categories': category_count,
'compare_enabled': compare_enabled,
'kpi_previous_total': previous_total,
'kpi_difference': kpi_difference,
'kpi_difference_abs': kpi_difference_abs,
'kpi_percentage': percentage,
'category_comparison': category_comparison,
'kpi_trend': kpi_trend,
'accounts':accounts,
'selected_account': account_id,
'selected_account_obj': selected_account_obj,
'kpi_balance': kpi_balance,
'accounts_charts': accounts_charts,
'period':period,
})
@login_required
def tag_list(request):
tags = Tag.objects.filter(owner=request.user)
return render(
request,
'expenses/tag_list.html',
{'active_menu': 'tags','tags':tags}
)
@login_required
def tag_create(request):
if request.method == 'POST':
form = TagForm(request.POST)
if form.is_valid():
tag = form.save(commit=False)
tag.owner = request.user
tag.save()
messages.success(request, 'Etiqueta creada correctamente.')
return redirect('tag_list')
else:
form = TagForm()
return render(
request,
'expenses/tag_form.html',
{'active_menu': 'tags','form': form}
)
@login_required
def tag_edit(request, pk):
tag = get_object_or_404(
Tag,
pk=pk,
owner=request.user
)
if request.method == 'POST':
form = TagForm(request.POST, instance=tag)
if form.is_valid():
form.save()
messages.success(request, 'Etiqueta actualizada.')
return redirect('tag_list')
else:
form = TagForm(instance=tag)
return render(
request,
'expenses/tag_form.html',
{'active_menu': 'tags','form': form}
)
@login_required
def tag_delete(request, pk):
tag = get_object_or_404(
Tag,
pk=pk,
owner=request.user
)
if request.method == 'POST':
tag.delete()
messages.success(request, 'Etiqueta eliminada.')
return redirect('tag_list')
return render(
request,
'expenses/tag_confirm_delete.html',
{'active_menu': 'tags','tag': tag}
)
@login_required
def account_list(request):
accounts = Account.objects.filter(owner=request.user)
return render(
request,
'expenses/account_list.html',
{'active_menu': 'accounts','accounts': accounts}
)
@login_required
def account_create(request):
if request.method == 'POST':
form = AccountForm(request.POST)
if form.is_valid():
account = form.save(commit=False)
account.owner = request.user
account.save()
messages.success(request, 'Cuenta creada correctamente.')
return redirect('account_list')
else:
form = AccountForm()
return render(
request,
'expenses/account_form.html',
{'active_menu': 'accounts','form': form}
)
@login_required
def account_edit(request, pk):
account = get_object_or_404(
Account,
pk=pk,
owner=request.user
)
if request.method == 'POST':
form = AccountForm(request.POST, instance=account)
if form.is_valid():
form.save()
messages.success(request, 'Cuenta actualizada.')
return redirect('account_list')
else:
form = AccountForm(instance=account)
return render(
request,
'expenses/account_form.html',
{'active_menu': 'accounts','form': form}
)
@login_required
def account_delete(request, pk):
account = get_object_or_404(Account, pk=pk, owner=request.user)
if request.method == 'POST':
account.active = False
account.save()
messages.success(request, 'Cuenta eliminada.')
return redirect('account_list')
return render(
request,
'expenses/account_confirm_delete.html',
{'active_menu': 'accounts','account':account}
)
@login_required
def income_create(request):
if request.method == 'POST':
form = IncomeForm(request.POST, user=request.user)
if form.is_valid():
income = form.save(commit=False)
income.owner = request.user
income.save()
messages.success(request, 'Ingreso creado correctamente.')
return redirect('income_list')
else:
form = IncomeForm(user=request.user)
return render(
request,
'expenses/income_form.html',
{'active_menu': 'incomes','form': form}
)
@login_required
def income_list(request):
incomes = Income.objects.filter(owner=request.user)
return render(
request,
'expenses/income_list.html',
{'active_menu': 'incomes','incomes': incomes}
)
@login_required
def income_edit(request, pk):
income = get_object_or_404(
Income,
pk=pk,
owner=request.user
)
if request.method == 'POST':
form = IncomeForm(request.POST, instance=income, user=request.user)
if form.is_valid():
form.save()
messages.success(request, 'Ingreso actualizado.')
return redirect('income_list')
else:
form = IncomeForm(instance=income, user=request.user)
return render(
request,
'expenses/income_form.html',
{'active_menu': 'incomes','form': form}
)
@login_required
def income_delete(request, pk):
income = get_object_or_404(Income, pk=pk, owner=request.user)
if request.method == 'POST':
income.delete()
messages.success(request, 'Ingreso eliminado.')
return redirect('income_list')
return render(
request,
'expenses/income_confirm_delete.html',
{'active_menu': 'incomes','income':income}
)
@login_required
def fuel_create(request):
if request.method == 'POST':
form = FuelEntryForm(request.POST, user=request.user)
if form.is_valid():
expense = Expense.objects.create(
owner=request.user,
date=form.cleaned_data['date'],
amount=form.cleaned_data['amount'],
account=form.cleaned_data['account'],
category=Category.objects.get(slug='gasolina'),
description='Repostaje',
)
FuelEntry.objects.create(
expense=expense,
odometer=form.cleaned_data['odometer'],
liters=form.cleaned_data['liters'],
)
return redirect('fuel_list')
else:
form = FuelEntryForm(user=request.user)
return render(request, 'fuel/create.html',{
'active_menu': 'fuel',
'form': form
})
@login_required
def fuel_list(request):
selected_year = request.GET.get('year')
current_year = datetime.now().year
selected_year = int(selected_year) if selected_year else current_year
fuels = FuelEntry.objects.filter(
expense__owner=request.user,
expense__date__year=selected_year
).select_related('expense').order_by('expense__date')
monthly_expenses = (
fuels
.annotate(month=ExtractMonth('expense__date'))
.values('month')
.annotate(total=Sum('expense__amount'))
)
month_map = {m['month']: float(m['total']) for m in monthly_expenses}
monthly_data = [
month_map.get(month, 0)
for month in range(1, 13)
]
year_list = (
FuelEntry.objects.filter(
expense__owner=request.user,)
.annotate(year=ExtractYear('expense__date'))
.values_list('year', flat=True)
.distinct()
.order_by('year')
)
km_data = []
dates = []
for fuel in fuels:
km = fuel.km_since_previous()
if km:
km_data.append(km)
dates.append(fuel.expense.date.strftime('%Y-%m-%d'))
return render( request, 'fuel/list.html',
{
'active_menu': 'fuel',
'fuels': fuels,
'monthly_data': monthly_data,
'km_data': km_data,
'km_dates': dates,
'selected_year': selected_year,
'year_list': year_list,
}
)
@login_required
def fuel_edit(request, pk):
expense = get_object_or_404(
Expense,
pk=pk,
owner=request.user,
)
fuel = get_object_or_404(FuelEntry, expense=expense)
if request.method == "POST":
form = FuelEntryForm(
request.POST,
user=request.user
)
if form.is_valid():
# Update expense
expense.date = form.cleaned_data['date']
expense.amount = form.cleaned_data['amount']
expense.account = form.cleaned_data['account']
expense.description = 'Repostaje'
expense.save()
# Update FuelEntry
fuel.odometer = form.cleaned_data['odometer']
fuel.liters = form.cleaned_data['liters']
fuel.save()
return redirect('expense_list')
else:
fuel = expense.fuel_data
# Initialize manually
form = FuelEntryForm(
initial={
'date': expense.date,
'amount': expense.amount,
'account': expense.account,
'odometer': fuel.odometer,
'liters': fuel.liters
},
user=request.user
)
return render(
request, 'fuel/create.html', {
'active_menu': 'expenses',
'form': form,
'editing': True,
}
)
@login_required
def category_list(request):
categories = Category.objects.filter(
owner=request.user
)
if request.method == "POST":
form = CategoryForm(request.POST)
if form.is_valid():
category = form.save(commit=False)
category.owner = request.user
category.save()
return redirect('category_list')
else:
form = CategoryForm()
return render(request, 'categories/list.html', {
'active_menu': 'categories',
'categories': categories,
'form': form,
})