From bde0636331cffae7ad5b807913c0edc9d2017be5 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Tue, 17 Feb 2026 13:51:37 +0100 Subject: [PATCH] Created fuel section and the user can create categories --- expenses_manager/expenses/forms.py | 27 ++++- .../expenses/migrations/0005_fuelentry.py | 27 +++++ .../expenses/migrations/0006_category_slug.py | 18 +++ .../0007_alter_category_options_and_more.py | 23 ++++ expenses_manager/expenses/models.py | 51 +++++++- .../expenses/templates/categories/list.html | 38 ++++++ .../expenses/templates/expenses/base.html | 5 +- .../expenses/templates/fuel/create.html | 21 ++++ .../expenses/templates/fuel/list.html | 82 +++++++++++++ expenses_manager/expenses/urls.py | 3 + expenses_manager/expenses/views.py | 110 +++++++++++++++++- 11 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 expenses_manager/expenses/migrations/0005_fuelentry.py create mode 100644 expenses_manager/expenses/migrations/0006_category_slug.py create mode 100644 expenses_manager/expenses/migrations/0007_alter_category_options_and_more.py create mode 100644 expenses_manager/expenses/templates/categories/list.html create mode 100644 expenses_manager/expenses/templates/fuel/create.html create mode 100644 expenses_manager/expenses/templates/fuel/list.html 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

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ +
+ +

Listado

+ + + + + + + + + {% for category in categories %} + + + + + {% endfor %} + +
CategoríaCategoría padre
{{ category.name }}{% if category.parent %}{{ category.parent.name }}{% endif %}
+ +{% 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 @@ Cuentas Ingresos Etiquetas + Repostajes - + Hola {{ request.user.username }} - +
{% csrf_token %} diff --git a/expenses_manager/expenses/templates/fuel/create.html b/expenses_manager/expenses/templates/fuel/create.html new file mode 100644 index 0000000..c440728 --- /dev/null +++ b/expenses_manager/expenses/templates/fuel/create.html @@ -0,0 +1,21 @@ +{% extends "expenses/base.html" %} + +{% block title %} + Nuevo repostaje +{% endblock %} + +{% block content %} +

+ Nuevo repostaje +

+ + + {% csrf_token %} + {{ form.as_p }} + +
+ + 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 + + + + + + + + + + + + + + + + {% for fuel in fuels %} + + + + + + + + + {% endfor %} + +
FechaKilometrajeLitrosGasto€/LKm desde anterior
{{ fuel.expense.date }}{{ fuel.odometer }}{{ fuel.liters }}{{ fuel.expense.amount }}{{ fuel.price_per_liter|floatformat:2 }}{{ fuel.km_since_previous }}
+ +
+ +
+ + + + + + +{% 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