diff --git a/expenses_manager/expenses/forms.py b/expenses_manager/expenses/forms.py
index 40dea2d..12f310b 100644
--- a/expenses_manager/expenses/forms.py
+++ b/expenses_manager/expenses/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from .models import Expense, Category, Income, Tag, Account
+from .models import Expense, Category, Income, Tag, Account, FuelEntry
class ExpenseForm(forms.ModelForm):
@@ -55,4 +55,27 @@ class IncomeForm(forms.ModelForm):
self.fields['account'].queryset = Account.objects.filter(
owner=user,
active=True,
- )
\ No newline at end of file
+ )
+
+class FuelEntryForm(forms.Form):
+ # Expense fields
+ date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
+ amount = forms.DecimalField(max_digits=10, decimal_places=2)
+ account = forms.ModelChoiceField(queryset=None)
+
+ # Specifics fuel fields
+ odometer = forms.IntegerField(label='kilometraje actual')
+ liters = forms.DecimalField(max_digits=8, decimal_places=2)
+
+ def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user')
+ super().__init__(*args, **kwargs)
+
+ self.fields['account'].queryset = (
+ user.accounts.filter(active=True)
+ )
+
+class CategoryForm(forms.ModelForm):
+ class Meta:
+ model = Category
+ fields = ['name', 'parent']
\ No newline at end of file
diff --git a/expenses_manager/expenses/migrations/0005_fuelentry.py b/expenses_manager/expenses/migrations/0005_fuelentry.py
new file mode 100644
index 0000000..f2857fa
--- /dev/null
+++ b/expenses_manager/expenses/migrations/0005_fuelentry.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.2.10 on 2026-02-17 11:58
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('expenses', '0004_income'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FuelEntry',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('odometer', models.PositiveIntegerField()),
+ ('liters', models.DecimalField(decimal_places=2, max_digits=8)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('expense', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='fuel_data', to='expenses.expense')),
+ ],
+ options={
+ 'ordering': ['odometer'],
+ },
+ ),
+ ]
diff --git a/expenses_manager/expenses/migrations/0006_category_slug.py b/expenses_manager/expenses/migrations/0006_category_slug.py
new file mode 100644
index 0000000..d916425
--- /dev/null
+++ b/expenses_manager/expenses/migrations/0006_category_slug.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2.10 on 2026-02-17 12:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('expenses', '0005_fuelentry'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='category',
+ name='slug',
+ field=models.SlugField(blank=True),
+ ),
+ ]
diff --git a/expenses_manager/expenses/migrations/0007_alter_category_options_and_more.py b/expenses_manager/expenses/migrations/0007_alter_category_options_and_more.py
new file mode 100644
index 0000000..aba9844
--- /dev/null
+++ b/expenses_manager/expenses/migrations/0007_alter_category_options_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.10 on 2026-02-17 12:23
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('expenses', '0006_category_slug'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='category',
+ options={'ordering': ['name'], 'verbose_name_plural': 'categories'},
+ ),
+ migrations.AlterUniqueTogether(
+ name='category',
+ unique_together={('name', 'parent', 'owner', 'slug')},
+ ),
+ ]
diff --git a/expenses_manager/expenses/models.py b/expenses_manager/expenses/models.py
index 9f4723e..f7d1981 100644
--- a/expenses_manager/expenses/models.py
+++ b/expenses_manager/expenses/models.py
@@ -5,9 +5,12 @@ from django.conf import settings
from django.db.models import Sum
from django.db.models.fields import related
from django.db.models.functions import ExtractMonth
+from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100)
+ slug = models.SlugField(blank=True)
+
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@@ -23,13 +26,19 @@ class Category(models.Model):
)
class Meta:
- unique_together = ("name", "parent", "owner")
+ unique_together = ("name", "parent", "owner", "slug")
verbose_name_plural = "categories"
+ ordering = ["name"]
def __str__(self):
return self.name
-
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ from django.utils.text import slugify
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
class Account(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -211,3 +220,41 @@ class Income(models.Model):
def __str__(self):
return f'{self.name} - {self.amount}'
+
+
+class FuelEntry(models.Model):
+ expense = models.OneToOneField(
+ Expense,
+ on_delete=models.CASCADE,
+ related_name="fuel_data"
+ )
+
+ odometer = models.PositiveIntegerField() # kilometers
+ liters = models.DecimalField(max_digits=8, decimal_places=2)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ ordering = ['odometer']
+
+ def km_since_previous(self):
+ previous = FuelEntry.objects.filter(
+ expense__owner=self.expense.owner,
+ odometer__lt=self.odometer
+ ).order_by('-odometer').first()
+
+ if previous:
+ return self.odometer - previous.odometer
+
+ return None
+
+ def price_per_liter(self):
+ if self.liters:
+ return self.expense.amount / self.liters
+ return 0
+
+ def consumption(self):
+ km = self.km_since_previous()
+ if km and km > 0:
+ return (self.liters / km) * 100
+ return None
diff --git a/expenses_manager/expenses/templates/categories/list.html b/expenses_manager/expenses/templates/categories/list.html
new file mode 100644
index 0000000..f89d645
--- /dev/null
+++ b/expenses_manager/expenses/templates/categories/list.html
@@ -0,0 +1,38 @@
+{% extends "expenses/base.html" %}
+
+{% block title %}
+ Categorías
+{% endblock %}
+
+{% block content %}
+
+
Mis categorías
+
+ Nueva categoría
+
+
+
+
+ Listado
+
+
+
+ | Categoría |
+ Categoría padre |
+
+
+
+ {% for category in categories %}
+
+ | {{ category.name }} |
+ {% if category.parent %}{{ category.parent.name }}{% endif %} |
+
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/expenses_manager/expenses/templates/expenses/base.html b/expenses_manager/expenses/templates/expenses/base.html
index d59d43a..9429282 100644
--- a/expenses_manager/expenses/templates/expenses/base.html
+++ b/expenses_manager/expenses/templates/expenses/base.html
@@ -19,12 +19,13 @@
+
-
+
Hola {{ request.user.username }}
-
+
+
+ Volver
+{% endblock %}
\ No newline at end of file
diff --git a/expenses_manager/expenses/templates/fuel/list.html b/expenses_manager/expenses/templates/fuel/list.html
new file mode 100644
index 0000000..d6e39bd
--- /dev/null
+++ b/expenses_manager/expenses/templates/fuel/list.html
@@ -0,0 +1,82 @@
+{% extends "expenses/base.html" %}
+
+{% block title %}
+ Repostajes
+{% endblock %}
+
+{% block content %}
+
+ Mis repostajes
+
+ ➕ Nuevo repostaje
+
+
+
+
+
+
+ | Fecha |
+ Kilometraje |
+ Litros |
+ Gasto |
+ €/L |
+ Km desde anterior |
+
+
+
+ {% for fuel in fuels %}
+
+ | {{ fuel.expense.date }} |
+ {{ fuel.odometer }} |
+ {{ fuel.liters }} |
+ {{ fuel.expense.amount }} |
+ {{ fuel.price_per_liter|floatformat:2 }} |
+ {{ fuel.km_since_previous }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/expenses_manager/expenses/urls.py b/expenses_manager/expenses/urls.py
index cc0a858..faf11be 100644
--- a/expenses_manager/expenses/urls.py
+++ b/expenses_manager/expenses/urls.py
@@ -20,4 +20,7 @@ urlpatterns = [
path('incomes/new/', views.income_create, name='income_create'),
path('incomes//edit/', views.income_edit, name='income_edit'),
path('incomes//delete/', views.income_delete, name='income_delete'),
+ path('fuel/', views.fuel_list, name='fuel_list'),
+ path('fuel/create/', views.fuel_create, name='fuel_create'),
+ path('categories/', views.category_list, name='category_list'),
]
\ No newline at end of file
diff --git a/expenses_manager/expenses/views.py b/expenses_manager/expenses/views.py
index 511e389..bea3029 100644
--- a/expenses_manager/expenses/views.py
+++ b/expenses_manager/expenses/views.py
@@ -1,8 +1,8 @@
-from datetime import date
from operator import truediv
+from datetime import date, datetime
from django.contrib import messages
-from .models import Account, Category, Expense, Tag, Income
-from .forms import ExpenseForm, IncomeForm, TagForm, AccountForm
+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
@@ -748,3 +748,107 @@ def income_delete(request, pk):
{'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
+
+ if selected_year:
+ selected_year = int(selected_year)
+ else:
+ selected_year = 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)
+ ]
+
+ 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': range(current_year - 5, current_year + 1),
+ }
+ )
+
+@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,
+ })
+
\ No newline at end of file