147 lines
7.0 KiB
Python
147 lines
7.0 KiB
Python
from decimal import Decimal
|
|
|
|
from django.contrib import messages
|
|
from django.db.models import Sum
|
|
from django.http import HttpResponse, JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_GET, require_http_methods, require_POST
|
|
|
|
from .forms import ReportFilterForm, TripFilterForm, TripForm
|
|
from .models import Trip
|
|
from .utils import apply_trip_filters, calculate_google_maps_distance, export_trips_csv, report_queryset, summarize_trips
|
|
|
|
|
|
DEFAULT_META_DESCRIPTION = "Mobile-friendly mileage logging with odometer history, Google Maps assist, and IRS-style reporting."
|
|
|
|
|
|
def latest_trip_with_odometer():
|
|
return Trip.objects.exclude(end_odometer__isnull=True).order_by("-date", "-end_time", "-created_at").first()
|
|
|
|
|
|
def home(request):
|
|
today = timezone.localdate()
|
|
current_month = Trip.objects.filter(date__year=today.year, date__month=today.month)
|
|
ytd = Trip.objects.filter(date__year=today.year)
|
|
recent_trips = Trip.objects.all()[:5]
|
|
|
|
month_business = current_month.filter(trip_type=Trip.TripType.BUSINESS).aggregate(total=Sum("distance_miles"))["total"] or Decimal("0.0")
|
|
ytd_business = ytd.filter(trip_type=Trip.TripType.BUSINESS).aggregate(total=Sum("distance_miles"))["total"] or Decimal("0.0")
|
|
business_trip_count = current_month.filter(trip_type=Trip.TripType.BUSINESS).count()
|
|
last_trip = Trip.objects.order_by("-date", "-end_time", "-created_at").first()
|
|
latest_odo = latest_trip_with_odometer()
|
|
|
|
context = {
|
|
"project_name": "MileLedger",
|
|
"meta_title": "MileLedger | IRS-friendly mileage tracking for mobile",
|
|
"meta_description": DEFAULT_META_DESCRIPTION,
|
|
"month_business": month_business,
|
|
"ytd_business": ytd_business,
|
|
"business_trip_count": business_trip_count,
|
|
"recent_trips": recent_trips,
|
|
"last_trip": last_trip,
|
|
"latest_end_odometer": latest_odo.end_odometer if latest_odo else None,
|
|
"report_month": today.month,
|
|
"report_year": today.year,
|
|
"empty_state": not Trip.objects.exists(),
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def trip_create(request):
|
|
latest_odo = latest_trip_with_odometer()
|
|
latest_end_odometer = latest_odo.end_odometer if latest_odo else None
|
|
if request.method == "POST":
|
|
form = TripForm(request.POST, latest_end_odometer=latest_end_odometer)
|
|
if form.is_valid():
|
|
trip = form.save()
|
|
messages.success(request, "Trip saved. Your mileage log is updated and ready for review.")
|
|
return redirect("trip_detail", pk=trip.pk)
|
|
else:
|
|
now = timezone.localtime(timezone.now())
|
|
form = TripForm(initial={"date": timezone.localdate(), "start_time": now.strftime("%H:%M"), "end_time": now.strftime("%H:%M"), "trip_type": Trip.TripType.BUSINESS}, latest_end_odometer=latest_end_odometer)
|
|
|
|
return render(request, "core/trip_form.html", {"form": form, "page_heading": "Add a mileage entry", "submit_label": "Save trip", "meta_title": "Log a trip | MileLedger", "meta_description": DEFAULT_META_DESCRIPTION, "latest_end_odometer": latest_end_odometer, "trip": None})
|
|
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def trip_update(request, pk):
|
|
trip = get_object_or_404(Trip, pk=pk)
|
|
if request.method == "POST":
|
|
form = TripForm(request.POST, instance=trip)
|
|
if form.is_valid():
|
|
trip = form.save()
|
|
messages.success(request, "Trip updated. Your mileage history now reflects the latest details.")
|
|
return redirect("trip_detail", pk=trip.pk)
|
|
else:
|
|
form = TripForm(instance=trip)
|
|
|
|
return render(request, "core/trip_form.html", {"form": form, "page_heading": "Update trip details", "submit_label": "Save changes", "meta_title": "Edit trip | MileLedger", "meta_description": DEFAULT_META_DESCRIPTION, "trip": trip, "latest_end_odometer": None})
|
|
|
|
|
|
@require_GET
|
|
def trip_list(request):
|
|
form = TripFilterForm(request.GET or None)
|
|
trips = Trip.objects.all()
|
|
if form.is_valid():
|
|
trips = apply_trip_filters(trips, form.cleaned_data)
|
|
context = {"form": form, "trips": trips[:100], "summary": summarize_trips(trips), "meta_title": "Trip history | MileLedger", "meta_description": "Review, filter, and audit mileage entries."}
|
|
return render(request, "core/trip_list.html", context)
|
|
|
|
|
|
@require_GET
|
|
def trip_detail(request, pk):
|
|
trip = get_object_or_404(Trip, pk=pk)
|
|
return render(request, "core/trip_detail.html", {"trip": trip, "meta_title": f"Trip on {trip.date:%b %d, %Y} | MileLedger", "meta_description": "Trip detail and audit-ready timestamps."})
|
|
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def trip_delete(request, pk):
|
|
trip = get_object_or_404(Trip, pk=pk)
|
|
if request.method == "POST":
|
|
trip.delete()
|
|
messages.success(request, "Trip deleted.")
|
|
return redirect("trip_list")
|
|
return render(request, "core/trip_detail.html", {"trip": trip, "confirm_delete": True, "meta_title": "Delete trip | MileLedger", "meta_description": "Confirm trip deletion."})
|
|
|
|
|
|
@require_GET
|
|
def report_view(request):
|
|
today = timezone.localdate()
|
|
initial = {"report_type": "month", "month": today.month, "year": today.year}
|
|
form = ReportFilterForm(request.GET or initial)
|
|
trips = Trip.objects.none()
|
|
summary = {"business_miles": Decimal("0.0"), "non_business_miles": Decimal("0.0"), "total_miles": Decimal("0.0"), "trip_count": 0}
|
|
if form.is_valid():
|
|
trips = report_queryset(form.cleaned_data)
|
|
summary = summarize_trips(trips)
|
|
return render(request, "core/report.html", {"form": form, "trips": trips, "summary": summary, "meta_title": "IRS-style mileage report | MileLedger", "meta_description": "Generate monthly, annual, or custom mileage reports and export them to CSV."})
|
|
|
|
|
|
@require_GET
|
|
def report_export_csv(request):
|
|
today = timezone.localdate()
|
|
initial = {"report_type": "month", "month": today.month, "year": today.year}
|
|
form = ReportFilterForm(request.GET or initial)
|
|
if not form.is_valid():
|
|
messages.error(request, "Choose a valid report filter before exporting.")
|
|
return redirect("report_view")
|
|
|
|
trips = report_queryset(form.cleaned_data)
|
|
response = HttpResponse(export_trips_csv(trips), content_type="text/csv")
|
|
response["Content-Disposition"] = 'attachment; filename="mileage-report.csv"'
|
|
return response
|
|
|
|
|
|
@require_POST
|
|
def distance_estimate(request):
|
|
start_location = (request.POST.get("start_location") or "").strip()
|
|
end_location = (request.POST.get("end_location") or "").strip()
|
|
if not start_location or not end_location:
|
|
return JsonResponse({"ok": False, "message": "Enter both a start and destination first."}, status=400)
|
|
|
|
result = calculate_google_maps_distance(start_location, end_location)
|
|
status = 200 if result.ok else 422
|
|
return JsonResponse({"ok": result.ok, "miles": float(result.miles) if result.miles is not None else None, "message": result.message}, status=status)
|