39291-vm/core/forms.py
Flatlogic Bot 46ee143ab1 1.1
2026-04-01 16:28:08 +00:00

143 lines
7.8 KiB
Python

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