diff --git a/main/admin.py b/main/admin.py index 3b7cd99..b8fdae8 100644 --- a/main/admin.py +++ b/main/admin.py @@ -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) diff --git a/main/migrations/0001_initial.py b/main/migrations/0001_initial.py index fd8440e..2119b5e 100644 --- a/main/migrations/0001_initial.py +++ b/main/migrations/0001_initial.py @@ -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')], }, ), ] diff --git a/main/models.py b/main/models.py index 857d2da..dd21523 100644 --- a/main/models.py +++ b/main/models.py @@ -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): diff --git a/main/templates/main/fuelpurchase_list.html b/main/templates/main/fuelpurchase_list.html index c1603e5..ca8f13b 100644 --- a/main/templates/main/fuelpurchase_list.html +++ b/main/templates/main/fuelpurchase_list.html @@ -37,6 +37,7 @@
  • Average Litre Price: {{ summary.avg_price|floatformat:3|default:"0.000" }} €
  • Min Litre Price: {{ summary.min_price|floatformat:3|default:"0.000" }} €
  • Max Litre Price: {{ summary.max_price|floatformat:3|default:"0.000" }} €
  • +
  • Trips: {{ month_kilometers }} km
  • diff --git a/main/templates/main/monthly_trips.html b/main/templates/main/monthly_trips.html new file mode 100644 index 0000000..6c98d32 --- /dev/null +++ b/main/templates/main/monthly_trips.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load static %} +{% load extra_tags %} + + +{% block title %} Monthly trips {% endblock %} + + +{% block content %} + +
    +

    Monthly trips

    + +
    + + + + + {% for year in years %} + + {% endfor %} + + + + {% for month_num in 1|to_range:12 %} + + + {% for year in years %} + + {% endfor %} + + {% endfor %} + + + {% for year in years %} + + {% endfor %} + + +
    Month{{ year }}
    {{ month_names|dict_get:month_num }}{{ table_data|dict_get:month_num|dict_get:year }}
    Total (km){{ year_totals|dict_get:year }}
    +
    + +
    + +{% endblock %} \ No newline at end of file diff --git a/main/urls.py b/main/urls.py index 7ce37fc..e54478f 100644 --- a/main/urls.py +++ b/main/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path("", views.FuelPurchaseMonthlyListView.as_view(), name="fuelpurchase_list_current"), path("//", 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'), ] diff --git a/main/views.py b/main/views.py index 7b36187..ae769fb 100644 --- a/main/views.py +++ b/main/views.py @@ -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