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 .models import FuelPurchase
|
||||
from .models import FuelPurchase, MonthlyTrip
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
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("<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'),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue