Created fuel section and the user can create categories
This commit is contained in:
parent
415e51f062
commit
bde0636331
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
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):
|
class ExpenseForm(forms.ModelForm):
|
||||||
|
|
||||||
@ -55,4 +55,27 @@ class IncomeForm(forms.ModelForm):
|
|||||||
self.fields['account'].queryset = Account.objects.filter(
|
self.fields['account'].queryset = Account.objects.filter(
|
||||||
owner=user,
|
owner=user,
|
||||||
active=True,
|
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']
|
||||||
27
expenses_manager/expenses/migrations/0005_fuelentry.py
Normal file
27
expenses_manager/expenses/migrations/0005_fuelentry.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
expenses_manager/expenses/migrations/0006_category_slug.py
Normal file
18
expenses_manager/expenses/migrations/0006_category_slug.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -5,9 +5,12 @@ from django.conf import settings
|
|||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.db.models.fields import related
|
from django.db.models.fields import related
|
||||||
from django.db.models.functions import ExtractMonth
|
from django.db.models.functions import ExtractMonth
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
slug = models.SlugField(blank=True)
|
||||||
|
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -23,13 +26,19 @@ class Category(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("name", "parent", "owner")
|
unique_together = ("name", "parent", "owner", "slug")
|
||||||
verbose_name_plural = "categories"
|
verbose_name_plural = "categories"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
class Account(models.Model):
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
@ -211,3 +220,41 @@ class Income(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.name} - {self.amount}'
|
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
|
||||||
|
|||||||
38
expenses_manager/expenses/templates/categories/list.html
Normal file
38
expenses_manager/expenses/templates/categories/list.html
Normal 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 %}
|
||||||
@ -19,12 +19,13 @@
|
|||||||
<a href="{% url 'account_list' %}" class="nav-item {% if active_menu == 'accounts' %}active{% endif %} ">Cuentas</a>
|
<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 '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 '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="spacer"></span>
|
||||||
|
|
||||||
<span class="user">
|
<a href="{% url 'category_list' %}">
|
||||||
Hola {{ request.user.username }}
|
Hola {{ request.user.username }}
|
||||||
</span>
|
</a>
|
||||||
|
|
||||||
<form method="post" action="{% url 'logout' %}" class="logout-form">
|
<form method="post" action="{% url 'logout' %}" class="logout-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
21
expenses_manager/expenses/templates/fuel/create.html
Normal file
21
expenses_manager/expenses/templates/fuel/create.html
Normal 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 %}
|
||||||
82
expenses_manager/expenses/templates/fuel/list.html
Normal file
82
expenses_manager/expenses/templates/fuel/list.html
Normal 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 %}
|
||||||
@ -20,4 +20,7 @@ urlpatterns = [
|
|||||||
path('incomes/new/', views.income_create, name='income_create'),
|
path('incomes/new/', views.income_create, name='income_create'),
|
||||||
path('incomes/<int:pk>/edit/', views.income_edit, name='income_edit'),
|
path('incomes/<int:pk>/edit/', views.income_edit, name='income_edit'),
|
||||||
path('incomes/<int:pk>/delete/', views.income_delete, name='income_delete'),
|
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'),
|
||||||
]
|
]
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from datetime import date
|
|
||||||
from operator import truediv
|
from operator import truediv
|
||||||
|
from datetime import date, datetime
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import Account, Category, Expense, Tag, Income
|
from .models import Account, Category, Expense, FuelEntry, Tag, Income
|
||||||
from .forms import ExpenseForm, IncomeForm, TagForm, AccountForm
|
from .forms import ExpenseForm, IncomeForm, TagForm, AccountForm, FuelEntryForm, CategoryForm
|
||||||
# from dateutli.relativedelta import relativedelta
|
# from dateutli.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
@ -748,3 +748,107 @@ def income_delete(request, pk):
|
|||||||
{'active_menu': 'incomes','income':income}
|
{'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,
|
||||||
|
})
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user