import csv import json import os from dataclasses import dataclass from decimal import Decimal, ROUND_HALF_UP from io import StringIO from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import urlopen from django.db.models import DecimalField, Q, Sum, Value from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear from .models import Trip @dataclass class DistanceResult: ok: bool miles: Decimal | None = None message: str = "" def calculate_google_maps_distance(start_location: str, end_location: str) -> DistanceResult: api_key = os.getenv("GOOGLE_MAPS_API_KEY", "").strip() if not api_key: return DistanceResult(ok=False, message="Google Maps API key is not configured yet. Add GOOGLE_MAPS_API_KEY to enable route mileage.") params = urlencode({"origins": start_location, "destinations": end_location, "mode": "driving", "units": "imperial", "key": api_key}) url = f"https://maps.googleapis.com/maps/api/distancematrix/json?{params}" try: with urlopen(url, timeout=12) as response: payload = json.loads(response.read().decode("utf-8")) except (HTTPError, URLError, TimeoutError): return DistanceResult(ok=False, message="We could not reach Google Maps right now. Please try again or enter miles manually.") if payload.get("status") != "OK": return DistanceResult(ok=False, message="Google Maps could not validate that route. Please refine both addresses.") rows = payload.get("rows") or [] elements = (rows[0].get("elements") if rows else []) or [] element = elements[0] if elements else {} if element.get("status") != "OK": return DistanceResult(ok=False, message="Google Maps could not find that route. Try a fuller street address or city/state.") meters = element.get("distance", {}).get("value") if meters is None: return DistanceResult(ok=False, message="Google Maps did not return a distance for that route.") miles = (Decimal(str(meters)) / Decimal("1609.344")).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) return DistanceResult(ok=True, miles=miles, message="Driving mileage calculated from Google Maps.") def apply_trip_filters(queryset, data): if data.get("start_date"): queryset = queryset.filter(date__gte=data["start_date"]) if data.get("end_date"): queryset = queryset.filter(date__lte=data["end_date"]) if data.get("month"): queryset = queryset.annotate(filter_month=ExtractMonth("date")).filter(filter_month=data["month"]) if data.get("year"): queryset = queryset.annotate(filter_year=ExtractYear("date")).filter(filter_year=data["year"]) if data.get("trip_type"): queryset = queryset.filter(trip_type=data["trip_type"]) return queryset def report_queryset(cleaned_data): queryset = Trip.objects.all() report_type = cleaned_data.get("report_type") if report_type == "month": queryset = queryset.filter(date__month=cleaned_data["month"], date__year=cleaned_data["year"]) elif report_type == "range": queryset = queryset.filter(date__range=(cleaned_data["start_date"], cleaned_data["end_date"])) elif report_type == "year": queryset = queryset.filter(date__year=cleaned_data["year"]) return queryset.order_by("date", "start_time", "created_at") def summarize_trips(queryset): aggregates = queryset.aggregate( business_miles=Coalesce(Sum("distance_miles", filter=Q(trip_type=Trip.TripType.BUSINESS)), Value(Decimal("0.0")), output_field=DecimalField(max_digits=10, decimal_places=1)), non_business_miles=Coalesce(Sum("distance_miles", filter=~Q(trip_type=Trip.TripType.BUSINESS)), Value(Decimal("0.0")), output_field=DecimalField(max_digits=10, decimal_places=1)), total_miles=Coalesce(Sum("distance_miles"), Value(Decimal("0.0")), output_field=DecimalField(max_digits=10, decimal_places=1)), ) aggregates["trip_count"] = queryset.count() return aggregates def export_trips_csv(trips): buffer = StringIO() writer = csv.writer(buffer) writer.writerow(["Date of trip", "Start time", "End time", "Starting location", "Destination", "Business purpose", "Trip type", "Starting odometer", "Ending odometer", "Miles for this trip", "Distance source", "Notes", "Created at", "Updated at"]) for trip in trips: writer.writerow([trip.date, trip.start_time, trip.end_time, trip.start_location, trip.end_location, trip.business_purpose, trip.get_trip_type_display(), trip.start_odometer or "", trip.end_odometer or "", trip.distance_miles or "", trip.get_distance_source_display(), trip.notes, trip.created_at, trip.updated_at]) return buffer.getvalue()