from decimal import Decimal from django import forms from django.utils import timezone from .models import Trip DATETIME_LOCAL_FORMAT = "%Y-%m-%dT%H:%M" class TripForm(forms.ModelForm): created_at_override = forms.DateTimeField( required=False, label="Custom created timestamp", input_formats=[DATETIME_LOCAL_FORMAT], widget=forms.DateTimeInput(attrs={"type": "datetime-local", "class": "form-control", "placeholder": "Leave blank to use now"}, format=DATETIME_LOCAL_FORMAT), help_text="Optional for retroactive entries. Leave blank to use the current timestamp.", ) updated_at_override = forms.DateTimeField( required=False, label="Custom updated timestamp", input_formats=[DATETIME_LOCAL_FORMAT], widget=forms.DateTimeInput(attrs={"type": "datetime-local", "class": "form-control", "placeholder": "Leave blank to auto-update"}, format=DATETIME_LOCAL_FORMAT), help_text="Optional. If empty while editing, the app will stamp the current time.", ) update_end_odometer_from_map = forms.BooleanField( required=False, label="Use Google Maps miles to prefill ending odometer", help_text="If a starting odometer and route miles are available, this suggests an ending odometer.", ) class Meta: model = Trip fields = ["date", "start_time", "end_time", "start_location", "end_location", "business_purpose", "trip_type", "start_odometer", "end_odometer", "distance_miles", "notes"] widgets = { "date": forms.DateInput(attrs={"type": "date", "class": "form-control"}), "start_time": forms.TimeInput(attrs={"type": "time", "class": "form-control"}), "end_time": forms.TimeInput(attrs={"type": "time", "class": "form-control"}), "start_location": forms.TextInput(attrs={"class": "form-control", "placeholder": "123 Market St, San Francisco"}), "end_location": forms.TextInput(attrs={"class": "form-control", "placeholder": "Client office, warehouse, property, etc."}), "business_purpose": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Meeting with Client A at Property X"}), "trip_type": forms.Select(attrs={"class": "form-select"}), "start_odometer": forms.NumberInput(attrs={"class": "form-control", "step": "0.1", "min": "0", "placeholder": "Optional"}), "end_odometer": forms.NumberInput(attrs={"class": "form-control", "step": "0.1", "min": "0", "placeholder": "Optional"}), "distance_miles": forms.NumberInput(attrs={"class": "form-control", "step": "0.1", "min": "0", "placeholder": "Auto-filled from Google Maps or manual"}), "notes": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Client, project, repair details, or extra notes"}), } help_texts = { "business_purpose": "Be specific so the trip is audit-friendly.", "distance_miles": "Preferred source is odometer when both readings are present; otherwise Google Maps mileage is used.", } def __init__(self, *args, latest_end_odometer=None, **kwargs): super().__init__(*args, **kwargs) if not self.instance.pk and latest_end_odometer is not None and self.initial.get("start_odometer") in (None, ""): self.initial["start_odometer"] = latest_end_odometer def clean_business_purpose(self): purpose = (self.cleaned_data.get("business_purpose") or "").strip() if len(purpose) < 8: raise forms.ValidationError("Please add a more specific business purpose for this trip.") return purpose def clean(self): cleaned_data = super().clean() start_odometer = cleaned_data.get("start_odometer") end_odometer = cleaned_data.get("end_odometer") distance_miles = cleaned_data.get("distance_miles") should_prefill_end = cleaned_data.get("update_end_odometer_from_map") if start_odometer is not None and end_odometer is not None and end_odometer < start_odometer: self.add_error("end_odometer", "Ending odometer must be greater than or equal to starting odometer.") if should_prefill_end and start_odometer is not None and distance_miles is not None and not end_odometer: cleaned_data["end_odometer"] = Decimal(start_odometer) + Decimal(distance_miles) self.cleaned_data["end_odometer"] = cleaned_data["end_odometer"] if start_odometer is None and end_odometer is not None: self.add_error("start_odometer", "Add a starting odometer before storing an ending odometer.") if start_odometer is None and end_odometer is None and distance_miles is None: self.add_error("distance_miles", "Calculate Google Maps mileage or enter odometer readings.") return cleaned_data def save(self, commit=True): trip = super().save(commit=False) now = timezone.now() created_override = self.cleaned_data.get("created_at_override") updated_override = self.cleaned_data.get("updated_at_override") if trip.pk: trip.created_at = created_override or trip.created_at trip.updated_at = updated_override or now else: trip.created_at = created_override or now trip.updated_at = updated_override or trip.created_at if commit: trip.save() return trip class TripFilterForm(forms.Form): start_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date", "class": "form-control"})) end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date", "class": "form-control"})) month = forms.IntegerField(required=False, min_value=1, max_value=12, widget=forms.NumberInput(attrs={"class": "form-control", "placeholder": "Month"})) year = forms.IntegerField(required=False, min_value=2000, max_value=2100, widget=forms.NumberInput(attrs={"class": "form-control", "placeholder": "Year"})) trip_type = forms.ChoiceField(required=False, choices=[("", "All trip types"), *Trip.TripType.choices], widget=forms.Select(attrs={"class": "form-select"})) class ReportFilterForm(forms.Form): REPORT_CHOICES = [("month", "Monthly report"), ("range", "Custom date range"), ("year", "Annual summary")] report_type = forms.ChoiceField(choices=REPORT_CHOICES, required=False, initial="month", widget=forms.Select(attrs={"class": "form-select"})) month = forms.IntegerField(required=False, min_value=1, max_value=12, widget=forms.NumberInput(attrs={"class": "form-control"})) year = forms.IntegerField(required=False, min_value=2000, max_value=2100, widget=forms.NumberInput(attrs={"class": "form-control"})) start_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date", "class": "form-control"})) end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date", "class": "form-control"})) def clean(self): cleaned = super().clean() report_type = cleaned.get("report_type") or "month" year = cleaned.get("year") month = cleaned.get("month") start_date = cleaned.get("start_date") end_date = cleaned.get("end_date") if report_type == "month": if not month or not year: raise forms.ValidationError("Choose both month and year for the monthly report.") elif report_type == "range": if not start_date or not end_date: raise forms.ValidationError("Choose both start and end dates for a custom range.") if end_date < start_date: raise forms.ValidationError("End date must be on or after the start date.") elif report_type == "year": if not year: raise forms.ValidationError("Choose a year for the annual summary.") return cleaned