Overview
The expenses module provides simple yet effective expense tracking with categories, receipts, and user attribution.
Expense
Main expense record model.
Primary key for the expense. Auto-generated by Django.
Date when the expense occurred.
Expense amount.
max_digits: 10
decimal_places: 2
Reference to expense category.
to: ExpenseCategory
on_delete: PROTECT
- Prevents deletion of categories that have associated expenses
Detailed description of the expense.
Receipt or proof of expense document.
upload_to: ‘receipts/’
blank: True
null: True
- Stored in media/receipts/
User who recorded this expense.
to: User (Django auth)
on_delete: CASCADE
String Representation
def __str__(self):
return f'{self.date} - {self.amount} - {self.category}'
ExpenseCategory
Expense category model for classification.
Primary key for the category. Auto-generated by Django.
Category name.
max_length: 100
unique: True
- Must be unique across all categories
String Representation
def __str__(self):
return self.name
Usage Examples
Creating Expense Categories
from expenses.models import ExpenseCategory
# Create common expense categories
categories = [
"Ingredientes",
"Servicios Públicos",
"Salarios",
"Mantenimiento",
"Marketing",
"Alquiler",
"Equipamiento",
"Transporte",
"Impuestos",
"Otros"
]
for cat_name in categories:
ExpenseCategory.objects.get_or_create(name=cat_name)
Recording Expenses
from expenses.models import Expense, ExpenseCategory
from django.contrib.auth.models import User
from datetime import date
# Get category and user
categoria = ExpenseCategory.objects.get(name="Ingredientes")
user = User.objects.get(username="admin")
# Record expense without receipt
expense = Expense.objects.create(
date=date.today(),
amount=250000.00,
category=categoria,
description="Compra de verduras y frutas para la semana",
user=user
)
# Record expense with receipt
expense_con_recibo = Expense.objects.create(
date=date(2024, 3, 1),
amount=1500000.00,
category=ExpenseCategory.objects.get(name="Servicios Públicos"),
description="Factura de energía eléctrica - Marzo 2024",
receipt=uploaded_file, # Django UploadedFile object
user=user
)
Querying Expenses
from expenses.models import Expense
from django.db.models import Sum, Count, Avg
from datetime import date, timedelta
# Get all expenses for a date range
inicio = date(2024, 3, 1)
fin = date(2024, 3, 31)
expenses_mes = Expense.objects.filter(
date__gte=inicio,
date__lte=fin
)
# Get expenses by category
ingredientes = Expense.objects.filter(
category__name="Ingredientes"
)
# Get expenses by user
user_expenses = Expense.objects.filter(
user__username="admin"
)
# Get recent expenses (last 30 days)
hace_30_dias = date.today() - timedelta(days=30)
recientes = Expense.objects.filter(
date__gte=hace_30_dias
).order_by('-date')
Expense Analytics
from expenses.models import Expense
from django.db.models import Sum, Count, Avg
from datetime import date
# Total expenses for current month
hoy = date.today()
total_mes = Expense.objects.filter(
date__year=hoy.year,
date__month=hoy.month
).aggregate(total=Sum('amount'))['total']
print(f"Total gastos del mes: ${total_mes:,.2f}")
# Expenses by category (current month)
por_categoria = Expense.objects.filter(
date__year=hoy.year,
date__month=hoy.month
).values('category__name').annotate(
total=Sum('amount'),
count=Count('id')
).order_by('-total')
for item in por_categoria:
print(f"{item['category__name']}: ${item['total']:,.2f} ({item['count']} gastos)")
# Average expense amount
promedio = Expense.objects.aggregate(
avg=Avg('amount')
)['avg']
print(f"Gasto promedio: ${promedio:,.2f}")
# Top 5 largest expenses
top_gastos = Expense.objects.order_by('-amount')[:5]
for expense in top_gastos:
print(f"{expense.date}: ${expense.amount:,.2f} - {expense.description}")
Monthly Reports
from expenses.models import Expense
from django.db.models import Sum
from datetime import date
import calendar
def reporte_mensual(year, month):
"""Generate monthly expense report"""
expenses = Expense.objects.filter(
date__year=year,
date__month=month
)
# Total by category
por_categoria = expenses.values(
'category__name'
).annotate(
total=Sum('amount')
).order_by('-total')
# Total general
total_general = expenses.aggregate(
total=Sum('amount')
)['total'] or 0
# Number of expenses
num_gastos = expenses.count()
# Expenses with receipts
con_recibo = expenses.exclude(receipt='').count()
return {
'year': year,
'month': calendar.month_name[month],
'total': total_general,
'num_gastos': num_gastos,
'con_recibo': con_recibo,
'por_categoria': list(por_categoria)
}
# Generate report
reporte = reporte_mensual(2024, 3)
print(f"Reporte: {reporte['month']} {reporte['year']}")
print(f"Total: ${reporte['total']:,.2f}")
print(f"Número de gastos: {reporte['num_gastos']}")
print(f"Con recibo: {reporte['con_recibo']}")
Filtering with Q Objects
from expenses.models import Expense
from django.db.models import Q
from datetime import date
# Expenses over a certain amount OR in specific category
grandes_o_salarios = Expense.objects.filter(
Q(amount__gte=1000000) | Q(category__name="Salarios")
)
# Expenses without receipt AND over certain amount
sin_recibo_grandes = Expense.objects.filter(
Q(receipt__isnull=True) | Q(receipt=''),
amount__gte=500000
)
# Search in description
busqueda = Expense.objects.filter(
description__icontains="factura"
)
Managing Categories
from expenses.models import ExpenseCategory, Expense
from django.db.models import Sum, Count
# Get all categories with expense counts
categorias_con_uso = ExpenseCategory.objects.annotate(
num_gastos=Count('expense'),
total_gastado=Sum('expense__amount')
).order_by('-total_gastado')
for cat in categorias_con_uso:
print(f"{cat.name}: {cat.num_gastos} gastos, Total: ${cat.total_gastado or 0:,.2f}")
# Check if category can be deleted
categoria = ExpenseCategory.objects.get(name="Marketing")
if categoria.expense_set.exists():
print(f"Cannot delete '{categoria.name}' - has {categoria.expense_set.count()} expenses")
else:
categoria.delete()
print(f"Category '{categoria.name}' deleted")
# Rename category
categoria = ExpenseCategory.objects.get(name="Otros")
categoria.name = "Gastos Varios"
categoria.save()
Working with Receipts
from expenses.models import Expense
# Get expenses with receipts
con_recibo = Expense.objects.exclude(
Q(receipt__isnull=True) | Q(receipt='')
)
# Get expenses without receipts
sin_recibo = Expense.objects.filter(
Q(receipt__isnull=True) | Q(receipt='')
)
# Access receipt URL
expense = Expense.objects.get(id=1)
if expense.receipt:
print(f"Receipt URL: {expense.receipt.url}")
print(f"Receipt path: {expense.receipt.path}")
# Add receipt to existing expense
expense = Expense.objects.get(id=5)
expense.receipt = uploaded_file
expense.save()
Best Practices
Category Protection: The ExpenseCategory model uses on_delete=PROTECT to prevent accidental deletion of categories that have associated expenses. You must reassign or delete all expenses in a category before you can delete the category itself.
Receipt Storage: Receipt files are stored in the media/receipts/ directory. Ensure your Django settings have MEDIA_ROOT and MEDIA_URL configured properly:# settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
And in your URLs:# urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ... your patterns
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
User Attribution: Every expense is linked to the user who created it. This provides an audit trail and accountability for expense records. Consider implementing permissions to control who can view, create, or modify expenses.
Common Queries
Dashboard Statistics
from expenses.models import Expense
from django.db.models import Sum, Count
from datetime import date, timedelta
hoy = date.today()
# Today's expenses
hoy_total = Expense.objects.filter(
date=hoy
).aggregate(total=Sum('amount'))['total'] or 0
# This month's expenses
mes_total = Expense.objects.filter(
date__year=hoy.year,
date__month=hoy.month
).aggregate(total=Sum('amount'))['total'] or 0
# Last 7 days
semana = hoy - timedelta(days=7)
semana_total = Expense.objects.filter(
date__gte=semana
).aggregate(total=Sum('amount'))['total'] or 0
# Year to date
ano_total = Expense.objects.filter(
date__year=hoy.year
).aggregate(total=Sum('amount'))['total'] or 0
stats = {
'hoy': hoy_total,
'mes': mes_total,
'semana': semana_total,
'ano': ano_total
}