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 .models import FuelPurchase
from .models import FuelPurchase, MonthlyTrip
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
@ -11,19 +13,37 @@ class Migration(migrations.Migration):
]
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(
name='FuelPurchase',
fields=[
('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 (€)')),
('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)')),
('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={
'verbose_name': 'Fuel Purchase',
'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 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):
GAS_STATION_CHOICES = [
(0, '-'),
(0, 'Unknown'),
(1, 'Neste'),
(2, 'Teboil'),
(3, 'ABC'),
(4, 'Shell'),
(5, 'St1'),
(6, 'SEO'),
(7, 'ysi5'),
(7, 'Ysi5'),
]
OCTANE_CHOICES = [
@ -23,19 +44,20 @@ class FuelPurchase(models.Model):
]
CAR_CHOICES = [
(0, '_'),
(0, 'Unknown'),
(1, 'Renault'),
(2, 'Nissan'),
(3, 'Smart'),
]
purchase_date = models.DateField(null=False, blank=False, 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 (€)")
price_per_litre = models.DecimalField(null=False, blank=False, 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)")
octane = models.IntegerField(choices=OCTANE_CHOICES, default=95, verbose_name="Octane")
gas_station = models.IntegerField(choices=GAS_STATION_CHOICES, default=1, verbose_name="Gas Station")
car = models.IntegerField(choices=CAR_CHOICES, default=3, verbose_name="Car")
purchase_date = models.DateField(default=date.today, verbose_name="Purchase Date")
total_cost = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Total Cost (€)")
price_per_litre = models.DecimalField(max_digits=6, decimal_places=3, verbose_name="Price per Litre (€)")
amount_litres = models.DecimalField(max_digits=7, decimal_places=2, verbose_name="Amount (litres)")
octane = models.PositiveSmallIntegerField(choices=OCTANE_CHOICES, default=95, verbose_name="Octane")
gas_station = models.PositiveSmallIntegerField(choices=GAS_STATION_CHOICES, default=0, verbose_name="Gas Station")
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:
constraints = [
@ -51,25 +73,28 @@ class FuelPurchase(models.Model):
],
name='unique_fuel_purchase'
),
models.CheckConstraint(
check=Q(total_cost__gt=0) & Q(total_cost__lt=100),
name="total_cost_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"
),
models.CheckConstraint(check=Q(total_cost__gt=0) & Q(total_cost__lt=1000), name="total_cost_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_plural = "Fuel Purchases"
def save(self, *args, **kwargs):
if not self.amount_litres and 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)
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>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>Trips:</strong> {{ month_kilometers }} km</li>
</ul>
</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("<int:year>/<int:month>/", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list"),
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/logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
]

View file

@ -1,17 +1,15 @@
from calendar import monthrange
from collections import defaultdict
from datetime import date, timedelta
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
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.views.generic import CreateView, ListView
from django.views.generic import CreateView, ListView, TemplateView
from .forms import FuelPurchaseForm
from .models import FuelPurchase
from .models import FuelPurchase, MonthlyTrip
class FuelPurchaseMonthlyListView(ListView):
@ -25,17 +23,24 @@ class FuelPurchaseMonthlyListView(ListView):
self.current_year = year
self.current_month = month
start_date = date(year, month, 1)
end_date = date(year, month, monthrange(year, month)[1])
# Haetaan MonthlyTrip heti
self.monthly_trip = MonthlyTrip.objects.filter(year=year, month=month).first()
return FuelPurchase.objects.filter(
purchase_date__range=(start_date, end_date)
).order_by("-purchase_date")
if not self.monthly_trip:
# Jos ei ole olemassa, ei ole ostoksiakaan
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):
context = super().get_context_data(**kwargs)
qs = self.object_list
summary = qs.aggregate(
total_cost_sum=Sum("total_cost"),
total_litres_sum=Sum("amount_litres"),
@ -47,6 +52,7 @@ class FuelPurchaseMonthlyListView(ListView):
context["summary"] = summary
context["current_year"] = self.current_year
context["current_month"] = self.current_month
context["month_kilometers"] = self.monthly_trip.kilometers if self.monthly_trip else 0
# Kuukausinavigointi
current_date = date(self.current_year, self.current_month, 1)
@ -59,8 +65,9 @@ class FuelPurchaseMonthlyListView(ListView):
context["next_month"] = next_month.month
# Kaikki kuukaudet alasvetovalikkoon
all_dates = FuelPurchase.objects.dates("purchase_date", "month", order="DESC")
context["available_months"] = all_dates
context["available_months"] = FuelPurchase.objects.dates(
"purchase_date", "month", order="DESC"
)
return context
@ -68,5 +75,32 @@ class FuelPurchaseMonthlyListView(ListView):
class FuelPurchaseCreateView(LoginRequiredMixin, CreateView):
model = FuelPurchase
form_class = FuelPurchaseForm
template_name = "main/fuelpurchase_add.html"
template_name = "fuelpurchase_add.html"
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