Skip to main content

Overview

The expenses module provides simple yet effective expense tracking with categories, receipts, and user attribution.

Expense

Main expense record model.
id
AutoField
required
Primary key for the expense. Auto-generated by Django.
date
DateField
required
Date when the expense occurred.
amount
DecimalField
required
Expense amount.
  • max_digits: 10
  • decimal_places: 2
category
ForeignKey
required
Reference to expense category.
  • to: ExpenseCategory
  • on_delete: PROTECT
  • Prevents deletion of categories that have associated expenses
description
TextField
required
Detailed description of the expense.
receipt
FileField
Receipt or proof of expense document.
  • upload_to: ‘receipts/’
  • blank: True
  • null: True
  • Stored in media/receipts/
user
ForeignKey
required
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.
id
AutoField
required
Primary key for the category. Auto-generated by Django.
name
CharField
required
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
}

Build docs developers (and LLMs) love