Add MonthlyTrip model and MonthlyTripView
This commit is contained in:
parent
0a4f3da34c
commit
646845e44d
7 changed files with 169 additions and 42 deletions
|
@ -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)
|
||||||
|
|
|
@ -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')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
45
main/templates/main/monthly_trips.html
Normal file
45
main/templates/main/monthly_trips.html
Normal 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 %}
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue