Add MonthlyTrip model and MonthlyTripView

This commit is contained in:
Nyymix 2025-08-10 17:00:43 +03:00
parent 0a4f3da34c
commit 646845e44d
7 changed files with 169 additions and 42 deletions

View file

@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import FuelPurchase from .models import FuelPurchase, MonthlyTrip
admin.site.register(FuelPurchase) admin.site.register(FuelPurchase)
admin.site.register(MonthlyTrip)

View file

@ -1,5 +1,7 @@
# Generated by Django 5.2.5 on 2025-08-09 17:20 # Generated by Django 5.2.5 on 2025-08-10 13:10
import datetime
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -11,19 +13,37 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='MonthlyTrip',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField(verbose_name='Year')),
('month', models.PositiveSmallIntegerField(choices=[(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], verbose_name='Month')),
('kilometers', models.PositiveIntegerField(default=0, verbose_name='Kilometers')),
],
options={
'verbose_name': 'Monthly Trip',
'verbose_name_plural': 'Monthly Trips',
'constraints': [models.UniqueConstraint(fields=('year', 'month'), name='unique_monthly_trip')],
},
),
migrations.CreateModel( migrations.CreateModel(
name='FuelPurchase', name='FuelPurchase',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purchase_date', models.DateField(verbose_name='Purchase Date')), ('purchase_date', models.DateField(default=datetime.date.today, verbose_name='Purchase Date')),
('total_cost', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total Cost (€)')), ('total_cost', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total Cost (€)')),
('price_per_litre', models.DecimalField(decimal_places=3, max_digits=6, verbose_name='Price per Litre (€)')), ('price_per_litre', models.DecimalField(decimal_places=3, max_digits=6, verbose_name='Price per Litre (€)')),
('amount_litres', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Amount (litres)')), ('amount_litres', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Amount (litres)')),
('octane', models.PositiveSmallIntegerField(choices=[(95, '95'), (98, '98')], default=95, verbose_name='Octane')),
('gas_station', models.PositiveSmallIntegerField(choices=[(0, 'Unknown'), (1, 'Neste'), (2, 'Teboil'), (3, 'ABC'), (4, 'Shell'), (5, 'St1'), (6, 'SEO'), (7, 'Ysi5')], default=0, verbose_name='Gas Station')),
('car', models.PositiveSmallIntegerField(choices=[(0, 'Unknown'), (1, 'Renault'), (2, 'Nissan'), (3, 'Smart')], default=0, verbose_name='Car')),
('monthly_trip', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fuel_purchases', to='main.monthlytrip', verbose_name='Monthly Trip')),
], ],
options={ options={
'verbose_name': 'Fuel Purchase', 'verbose_name': 'Fuel Purchase',
'verbose_name_plural': 'Fuel Purchases', 'verbose_name_plural': 'Fuel Purchases',
'constraints': [models.UniqueConstraint(fields=('purchase_date', 'total_cost', 'price_per_litre', 'amount_litres'), name='unique_fuel_purchase'), models.CheckConstraint(condition=models.Q(('total_cost__gt', 0), ('total_cost__lt', 100)), name='total_cost_range'), models.CheckConstraint(condition=models.Q(('price_per_litre__gt', 0), ('price_per_litre__lt', 5)), name='price_per_litre_range'), models.CheckConstraint(condition=models.Q(('amount_litres__gt', 0), ('amount_litres__lt', 100)), name='amount_litres_range')], 'constraints': [models.UniqueConstraint(fields=('purchase_date', 'total_cost', 'price_per_litre', 'amount_litres', 'octane', 'gas_station', 'car'), name='unique_fuel_purchase'), models.CheckConstraint(condition=models.Q(('total_cost__gt', 0), ('total_cost__lt', 1000)), name='total_cost_range'), models.CheckConstraint(condition=models.Q(('price_per_litre__gt', 0), ('price_per_litre__lt', 5)), name='price_per_litre_range'), models.CheckConstraint(condition=models.Q(('amount_litres__gt', 0), ('amount_litres__lt', 100)), name='amount_litres_range')],
}, },
), ),
] ]

View file

@ -1,20 +1,41 @@
from datetime import date from datetime import date
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import Q
class MonthlyTrip(models.Model):
MONTH_CHOICES = [
(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'),
(5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'),
(9, 'September'), (10, 'October'), (11, 'November'), (12, 'December'),
]
year = models.PositiveIntegerField(verbose_name="Year")
month = models.PositiveSmallIntegerField(choices=MONTH_CHOICES, verbose_name="Month")
kilometers = models.PositiveIntegerField(default=0, verbose_name="Kilometers")
class Meta:
constraints = [
models.UniqueConstraint(fields=['year', 'month'], name='unique_monthly_trip')
]
verbose_name = "Monthly Trip"
verbose_name_plural = "Monthly Trips"
def __str__(self):
return f"{self.month:02d}.{self.year} : {self.kilometers} km"
class FuelPurchase(models.Model): class FuelPurchase(models.Model):
GAS_STATION_CHOICES = [ GAS_STATION_CHOICES = [
(0, '-'), (0, 'Unknown'),
(1, 'Neste'), (1, 'Neste'),
(2, 'Teboil'), (2, 'Teboil'),
(3, 'ABC'), (3, 'ABC'),
(4, 'Shell'), (4, 'Shell'),
(5, 'St1'), (5, 'St1'),
(6, 'SEO'), (6, 'SEO'),
(7, 'ysi5'), (7, 'Ysi5'),
] ]
OCTANE_CHOICES = [ OCTANE_CHOICES = [
@ -23,19 +44,20 @@ class FuelPurchase(models.Model):
] ]
CAR_CHOICES = [ CAR_CHOICES = [
(0, '_'), (0, 'Unknown'),
(1, 'Renault'), (1, 'Renault'),
(2, 'Nissan'), (2, 'Nissan'),
(3, 'Smart'), (3, 'Smart'),
] ]
purchase_date = models.DateField(null=False, blank=False, default=date.today, verbose_name="Purchase Date") purchase_date = models.DateField(default=date.today, verbose_name="Purchase Date")
total_cost = models.DecimalField(null=False, blank=False, max_digits=10, decimal_places=2, verbose_name="Total Cost (€)") total_cost = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Total Cost (€)")
price_per_litre = models.DecimalField(null=False, blank=False, max_digits=6, decimal_places=3, verbose_name="Price per Litre (€)") price_per_litre = models.DecimalField(max_digits=6, decimal_places=3, verbose_name="Price per Litre (€)")
amount_litres = models.DecimalField(null=False, blank=False, max_digits=7, decimal_places=2, verbose_name="Amount (litres)") amount_litres = models.DecimalField(max_digits=7, decimal_places=2, verbose_name="Amount (litres)")
octane = models.IntegerField(choices=OCTANE_CHOICES, default=95, verbose_name="Octane") octane = models.PositiveSmallIntegerField(choices=OCTANE_CHOICES, default=95, verbose_name="Octane")
gas_station = models.IntegerField(choices=GAS_STATION_CHOICES, default=1, verbose_name="Gas Station") gas_station = models.PositiveSmallIntegerField(choices=GAS_STATION_CHOICES, default=0, verbose_name="Gas Station")
car = models.IntegerField(choices=CAR_CHOICES, default=3, verbose_name="Car") car = models.PositiveSmallIntegerField(choices=CAR_CHOICES, default=0, verbose_name="Car")
monthly_trip = models.ForeignKey(MonthlyTrip, on_delete=models.CASCADE, related_name='fuel_purchases', verbose_name="Monthly Trip")
class Meta: class Meta:
constraints = [ constraints = [
@ -51,25 +73,28 @@ class FuelPurchase(models.Model):
], ],
name='unique_fuel_purchase' name='unique_fuel_purchase'
), ),
models.CheckConstraint( models.CheckConstraint(check=Q(total_cost__gt=0) & Q(total_cost__lt=1000), name="total_cost_range"),
check=Q(total_cost__gt=0) & Q(total_cost__lt=100), models.CheckConstraint(check=Q(price_per_litre__gt=0) & Q(price_per_litre__lt=5), name="price_per_litre_range"),
name="total_cost_range" models.CheckConstraint(check=Q(amount_litres__gt=0) & Q(amount_litres__lt=100), name="amount_litres_range"),
),
models.CheckConstraint(
check=Q(price_per_litre__gt=0) & Q(price_per_litre__lt=5),
name="price_per_litre_range"
),
models.CheckConstraint(
check=Q(amount_litres__gt=0) & Q(amount_litres__lt=100),
name="amount_litres_range"
),
] ]
verbose_name = "Fuel Purchase" verbose_name = "Fuel Purchase"
verbose_name_plural = "Fuel Purchases" verbose_name_plural = "Fuel Purchases"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.amount_litres and self.price_per_litre: if not self.amount_litres and self.price_per_litre:
self.amount_litres = self.total_cost / self.price_per_litre self.amount_litres = self.total_cost / self.price_per_litre
if not self.monthly_trip_id:
month = self.purchase_date.month
year = self.purchase_date.year
monthly_trip, created = MonthlyTrip.objects.get_or_create(
year=year,
month=month,
defaults={'kilometers': 0}
)
self.monthly_trip = monthly_trip
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):

View file

@ -37,6 +37,7 @@
<li><strong>Average Litre Price:</strong> {{ summary.avg_price|floatformat:3|default:"0.000" }} €</li> <li><strong>Average Litre Price:</strong> {{ summary.avg_price|floatformat:3|default:"0.000" }} €</li>
<li><strong>Min Litre Price:</strong> {{ summary.min_price|floatformat:3|default:"0.000" }} €</li> <li><strong>Min Litre Price:</strong> {{ summary.min_price|floatformat:3|default:"0.000" }} €</li>
<li><strong>Max Litre Price:</strong> {{ summary.max_price|floatformat:3|default:"0.000" }} €</li> <li><strong>Max Litre Price:</strong> {{ summary.max_price|floatformat:3|default:"0.000" }} €</li>
<li><strong>Trips:</strong> {{ month_kilometers }} km</li>
</ul> </ul>
</div> </div>

View file

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load static %}
{% load extra_tags %}
<!-- Title -->
{% block title %} Monthly trips {% endblock %}
<!-- Content -->
{% block content %}
<div class="uk-container uk-margin-top">
<h2>Monthly trips</h2>
<div class="uk-overflow-auto">
<table class="uk-table uk-table-striped uk-table-hover uk-table-small">
<thead>
<tr>
<th>Month</th>
{% for year in years %}
<th>{{ year }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for month_num in 1|to_range:12 %}
<tr>
<td><strong>{{ month_names|dict_get:month_num }}</strong></td>
{% for year in years %}
<td>{{ table_data|dict_get:month_num|dict_get:year }}</td>
{% endfor %}
</tr>
{% endfor %}
<tr class="uk-background-muted">
<td><strong>Total (km)</strong></td>
{% for year in years %}
<td><strong>{{ year_totals|dict_get:year }}</strong></td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View file

@ -7,6 +7,7 @@ urlpatterns = [
path("", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list_current"), path("", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list_current"),
path("<int:year>/<int:month>/", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list"), path("<int:year>/<int:month>/", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list"),
path("add/", views.FuelPurchaseCreateView.as_view(), name="fuelpurchase_add"), path("add/", views.FuelPurchaseCreateView.as_view(), name="fuelpurchase_add"),
path('trips/', views.MonthlyTripView.as_view(), name='monthly_trips'),
path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'), path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'), path('accounts/logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
] ]

View file

@ -1,17 +1,15 @@
from calendar import monthrange from calendar import monthrange
from collections import defaultdict
from datetime import date, timedelta from datetime import date, timedelta
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg, Max, Min, Sum from django.db.models import Avg, Max, Min, Sum
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.views.generic import CreateView, ListView from django.views.generic import CreateView, ListView, TemplateView
from .forms import FuelPurchaseForm from .forms import FuelPurchaseForm
from .models import FuelPurchase from .models import FuelPurchase, MonthlyTrip
class FuelPurchaseMonthlyListView(ListView): class FuelPurchaseMonthlyListView(ListView):
@ -25,17 +23,24 @@ class FuelPurchaseMonthlyListView(ListView):
self.current_year = year self.current_year = year
self.current_month = month self.current_month = month
start_date = date(year, month, 1) # Haetaan MonthlyTrip heti
end_date = date(year, month, monthrange(year, month)[1]) self.monthly_trip = MonthlyTrip.objects.filter(year=year, month=month).first()
return FuelPurchase.objects.filter( if not self.monthly_trip:
purchase_date__range=(start_date, end_date) # Jos ei ole olemassa, ei ole ostoksiakaan
).order_by("-purchase_date") return FuelPurchase.objects.none()
return (
FuelPurchase.objects
.filter(monthly_trip=self.monthly_trip)
.select_related("monthly_trip")
.order_by("-purchase_date")
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
qs = self.object_list qs = self.object_list
summary = qs.aggregate( summary = qs.aggregate(
total_cost_sum=Sum("total_cost"), total_cost_sum=Sum("total_cost"),
total_litres_sum=Sum("amount_litres"), total_litres_sum=Sum("amount_litres"),
@ -47,6 +52,7 @@ class FuelPurchaseMonthlyListView(ListView):
context["summary"] = summary context["summary"] = summary
context["current_year"] = self.current_year context["current_year"] = self.current_year
context["current_month"] = self.current_month context["current_month"] = self.current_month
context["month_kilometers"] = self.monthly_trip.kilometers if self.monthly_trip else 0
# Kuukausinavigointi # Kuukausinavigointi
current_date = date(self.current_year, self.current_month, 1) current_date = date(self.current_year, self.current_month, 1)
@ -59,8 +65,9 @@ class FuelPurchaseMonthlyListView(ListView):
context["next_month"] = next_month.month context["next_month"] = next_month.month
# Kaikki kuukaudet alasvetovalikkoon # Kaikki kuukaudet alasvetovalikkoon
all_dates = FuelPurchase.objects.dates("purchase_date", "month", order="DESC") context["available_months"] = FuelPurchase.objects.dates(
context["available_months"] = all_dates "purchase_date", "month", order="DESC"
)
return context return context
@ -68,5 +75,32 @@ class FuelPurchaseMonthlyListView(ListView):
class FuelPurchaseCreateView(LoginRequiredMixin, CreateView): class FuelPurchaseCreateView(LoginRequiredMixin, CreateView):
model = FuelPurchase model = FuelPurchase
form_class = FuelPurchaseForm form_class = FuelPurchaseForm
template_name = "main/fuelpurchase_add.html" template_name = "fuelpurchase_add.html"
success_url = "/" success_url = "/"
class MonthlyTripView(TemplateView):
template_name = 'main/monthly_trips.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
all_distances = MonthlyTrip.objects.all().order_by('year', 'month')
years = sorted(set(d.year for d in all_distances))
table_data = defaultdict(dict)
year_totals = defaultdict(int)
for entry in all_distances:
table_data[entry.month][entry.year] = entry.kilometers
year_totals[entry.year] += entry.kilometers
month_names = dict(MonthlyTrip.MONTH_CHOICES)
context.update({
'years': years,
'table_data': table_data,
'month_names': month_names,
'year_totals': year_totals,
})
return context