Created fuel section and the user can create categories

This commit is contained in:
JKuijperM 2026-02-17 13:51:37 +01:00
parent 415e51f062
commit bde0636331
11 changed files with 396 additions and 9 deletions

View File

@ -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,
)
)
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']

View File

@ -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'],
},
),
]

View File

@ -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),
),
]

View File

@ -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')},
),
]

View File

@ -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

View File

@ -0,0 +1,38 @@
{% extends "expenses/base.html" %}
{% block title %}
Categorías
{% endblock %}
{% block content %}
<h1>Mis categorías</h1>
<h3> Nueva categoría</h3>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Crear</button>
</form>
<hr>
<h3>Listado</h3>
<table>
<thead>
<tr>
<th>Categoría</th>
<th>Categoría padre</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td>{{ category.name }}</td>
<td>{% if category.parent %}{{ category.parent.name }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -19,12 +19,13 @@
<a href="{% url 'account_list' %}" class="nav-item {% if active_menu == 'accounts' %}active{% endif %} ">Cuentas</a>
<a href="{% url 'income_list' %}" class="nav-item {% if active_menu == 'incomes' %}active{% endif %} ">Ingresos</a>
<a href="{% url 'tag_list' %}" class="nav-item {% if active_menu == 'tags' %}active{% endif %} ">Etiquetas</a>
<a href="{% url 'fuel_list' %}" class="nav-item {% if active_menu == 'fuel' %}active{% endif %} ">Repostajes</a>
<span class="spacer"></span>
<span class="user">
<a href="{% url 'category_list' %}">
Hola {{ request.user.username }}
</span>
</a>
<form method="post" action="{% url 'logout' %}" class="logout-form">
{% csrf_token %}

View File

@ -0,0 +1,21 @@
{% extends "expenses/base.html" %}
{% block title %}
Nuevo repostaje
{% endblock %}
{% block content %}
<h1>
Nuevo repostaje
</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">
Crear repostaje
</button>
</form>
<a href="{% url 'fuel_list' %}">Volver</a>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "expenses/base.html" %}
{% block title %}
Repostajes
{% endblock %}
{% block content %}
<h1>Mis repostajes</h1>
<a class="btn" href="{% url 'fuel_create' %}"> Nuevo repostaje</a>
</div>
<table>
<thead>
<tr>
<th>Fecha</th>
<th>Kilometraje</th>
<th>Litros</th>
<th>Gasto</th>
<th>€/L</th>
<th>Km desde anterior</th>
</tr>
</thead>
<tbody>
{% for fuel in fuels %}
<tr>
<td>{{ fuel.expense.date }}</td>
<td>{{ fuel.odometer }}</td>
<td>{{ fuel.liters }}</td>
<td>{{ fuel.expense.amount }}</td>
<td>{{ fuel.price_per_liter|floatformat:2 }}</td>
<td>{{ fuel.km_since_previous }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<form method="get">
<select name="year" onchange="this.form.submit()">
{% for y in year_list %}
<option value="{{ y }}" {% if y == selected_year %}selected{% endif %}>
{{ y }}
</option>
{% endfor %}
</select>
</form>
<canvas id="monthlyChart"></canvas>
<canvas id="kmChart"></canvas>
<script>
const monthlyData = {{ monthly_data|safe }};
new Chart(document.getElementById('monthlyChart'), {
type: 'bar',
data: {
labels: ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'],
datasets: [{
label: 'Gasto mensual',
data: monthlyData
}]
}
});
const kmData = {{ km_data|safe }};
const kmDates = {{ km_dates|safe }};
new Chart(document.getElementById('kmChart'), {
type: 'line',
data: {
labels: kmDates,
datasets: [{
label: 'Km entre repostajes',
data: kmData
}]
}
});
</script>
{% endblock %}

View File

@ -20,4 +20,7 @@ urlpatterns = [
path('incomes/new/', views.income_create, name='income_create'),
path('incomes/<int:pk>/edit/', views.income_edit, name='income_edit'),
path('incomes/<int:pk>/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'),
]

View File

@ -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,
})