From d11c1f09c60f168431fdd599682272699e5e05e5 Mon Sep 17 00:00:00 2001 From: Danny McClelland Date: Thu, 19 Feb 2026 20:52:11 +0000 Subject: [PATCH 1/3] feat: add per-vehicle odometer unit setting Adds an `odometer_unit` field to the Vehicle model, allowing users to configure miles or km independently per vehicle. This is useful when managing vehicles with different odometer units (e.g. a UK car in miles and a foreign import or motorbike in km). - Add `odometer_unit` column to vehicles table (nullable, defaults to user pref) - Add `ODOMETER_UNITS` constant list and `get_effective_odometer_unit()` helper - Add odometer unit selector to vehicle create/edit form - Update fuel log list, fuel forms, and vehicle view to display the vehicle's effective unit rather than the global user preference - Update Tessie odometer/range display to respect per-vehicle unit - Create Alembic migration for the new column --- app/models.py | 21 +++++++++++++++ app/routes/api.py | 2 +- app/routes/main.py | 2 +- app/routes/vehicles.py | 10 ++++--- app/templates/fuel/form.html | 16 ++++++++---- app/templates/fuel/index.html | 2 +- app/templates/fuel/quick.html | 14 +++++++--- app/templates/vehicles/form.html | 12 +++++++++ app/templates/vehicles/view.html | 10 +++---- ...cdb1497c6_add_odometer_unit_to_vehicles.py | 26 +++++++++++++++++++ 10 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 migrations/versions/998cdb1497c6_add_odometer_unit_to_vehicles.py diff --git a/app/models.py b/app/models.py index 0520e1c..d45cd58 100644 --- a/app/models.py +++ b/app/models.py @@ -174,6 +174,9 @@ class Vehicle(db.Model): # Tracking unit (mileage or hours) tracking_unit = db.Column(db.String(20), default='mileage') # mileage, hours + # Per-vehicle odometer unit override (if None, falls back to user's distance_unit) + odometer_unit = db.Column(db.String(10), default=None) # km, mi, or None (use user preference) + # Fuel info fuel_type = db.Column(db.String(20), default='petrol') # petrol, diesel, electric, hybrid, lpg tank_capacity = db.Column(db.Float) # in liters @@ -219,6 +222,18 @@ class Vehicle(db.Model): charging_sessions = db.relationship('ChargingSession', backref='vehicle', lazy='dynamic', cascade='all, delete-orphan') + def get_effective_odometer_unit(self): + """Return the odometer unit for this vehicle. + + Uses the vehicle's own odometer_unit if set, otherwise falls back to + the owner's distance_unit preference. + """ + if self.odometer_unit: + return self.odometer_unit + if self.owner: + return self.owner.distance_unit + return 'km' + def get_total_fuel_cost(self): return sum(log.total_cost for log in self.fuel_logs.all() if log.total_cost) @@ -644,6 +659,12 @@ def get_all_branding(): ('hours', 'Hours'), ] +# Odometer unit options (for per-vehicle override) +ODOMETER_UNITS = [ + ('km', 'Kilometres (km)'), + ('mi', 'Miles (mi)'), +] + # Fuel types FUEL_TYPES = [ ('petrol', 'Petrol/Gasoline'), diff --git a/app/routes/api.py b/app/routes/api.py index bec9cab..f434977 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -295,7 +295,7 @@ def vehicle_stats(vehicle_id): 'expenses_by_category': category_totals, 'total_fuel_cost': vehicle.get_total_fuel_cost(), 'total_expense_cost': vehicle.get_total_expense_cost(), - 'total_distance': vehicle.get_total_distance(current_user.distance_unit), + 'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()), 'avg_consumption': vehicle.get_average_consumption() }) diff --git a/app/routes/main.py b/app/routes/main.py index 0891655..1ebf5bd 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -73,7 +73,7 @@ def dashboard(): ).scalar() total_expense_cost = expense_result or 0 - # Total distance + # Total distance (uses user's distance_unit for Tessie conversion; raw logs are in their stored unit) for vehicle in vehicles: total_distance += vehicle.get_total_distance(current_user.distance_unit) diff --git a/app/routes/vehicles.py b/app/routes/vehicles.py index 222737f..a192320 100644 --- a/app/routes/vehicles.py +++ b/app/routes/vehicles.py @@ -7,7 +7,7 @@ from flask_babel import gettext as _ from werkzeug.utils import secure_filename from app import db -from app.models import Vehicle, VehicleSpec, VehiclePart, FuelLog, Expense, User, Reminder, VEHICLE_TYPES, FUEL_TYPES, VEHICLE_SPEC_TYPES, REMINDER_TYPES, PART_TYPES, TRACKING_UNITS, AppSettings +from app.models import Vehicle, VehicleSpec, VehiclePart, FuelLog, Expense, User, Reminder, VEHICLE_TYPES, FUEL_TYPES, VEHICLE_SPEC_TYPES, REMINDER_TYPES, PART_TYPES, TRACKING_UNITS, ODOMETER_UNITS, AppSettings from app.services.tessie import TessieService bp = Blueprint('vehicles', __name__, url_prefix='/vehicles') @@ -47,6 +47,7 @@ def new(): name=request.form.get('name'), vehicle_type=request.form.get('vehicle_type'), tracking_unit=request.form.get('tracking_unit', 'mileage'), + odometer_unit=request.form.get('odometer_unit') or None, make=request.form.get('make'), model=request.form.get('model'), year=int(request.form.get('year')) if request.form.get('year') else None, @@ -95,6 +96,7 @@ def new(): vehicle_types=VEHICLE_TYPES, fuel_types=FUEL_TYPES, tracking_units=TRACKING_UNITS, + odometer_units=ODOMETER_UNITS, spec_types=VEHICLE_SPEC_TYPES, tessie_configured=tessie_configured) @@ -121,7 +123,7 @@ def view(vehicle_id): 'total_fuel_cost': vehicle.get_total_fuel_cost(), 'total_expense_cost': vehicle.get_total_expense_cost(), 'total_cost': vehicle.get_total_cost(), - 'total_distance': vehicle.get_total_distance(current_user.distance_unit), + 'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()), 'avg_consumption': vehicle.get_average_consumption(), 'fuel_logs_count': vehicle.fuel_logs.count(), 'expenses_count': vehicle.expenses.count() @@ -168,6 +170,7 @@ def edit(vehicle_id): vehicle.name = request.form.get('name') vehicle.vehicle_type = request.form.get('vehicle_type') vehicle.tracking_unit = request.form.get('tracking_unit', 'mileage') + vehicle.odometer_unit = request.form.get('odometer_unit') or None vehicle.make = request.form.get('make') vehicle.model = request.form.get('model') vehicle.year = int(request.form.get('year')) if request.form.get('year') else None @@ -224,6 +227,7 @@ def edit(vehicle_id): vehicle_types=VEHICLE_TYPES, fuel_types=FUEL_TYPES, tracking_units=TRACKING_UNITS, + odometer_units=ODOMETER_UNITS, spec_types=VEHICLE_SPEC_TYPES, specs=specs, tessie_configured=tessie_configured) @@ -360,7 +364,7 @@ def report(vehicle_id): 'total_fuel_cost': vehicle.get_total_fuel_cost(), 'total_expense_cost': vehicle.get_total_expense_cost(), 'total_cost': vehicle.get_total_cost(), - 'total_distance': vehicle.get_total_distance(current_user.distance_unit), + 'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()), 'avg_consumption': vehicle.get_average_consumption(), 'fuel_logs_count': len(fuel_logs), 'expenses_count': len(expenses) diff --git a/app/templates/fuel/form.html b/app/templates/fuel/form.html index 1f208d1..371a648 100644 --- a/app/templates/fuel/form.html +++ b/app/templates/fuel/form.html @@ -20,14 +20,15 @@

{% if log %}Ed {% for vehicle in vehicles %} {% endfor %} -

Last odometer: - {{ current_user.distance_unit }}

+

Last odometer: - {{ current_user.distance_unit }}

@@ -39,7 +40,7 @@

{% if log %}Ed

{% if last_odometer %}{{ _('Last') }}: {{ "{:,.0f}".format(last_odometer) }} {{ current_user.distance_unit }}{% endif %} @@ -120,11 +121,16 @@

{{ _('Quick Fuel En const select = document.getElementById('vehicle_id'); const option = select.options[select.selectedIndex]; const odometer = option.dataset.odometer; + const odometerUnit = option.dataset.odometerUnit || '{{ current_user.distance_unit }}'; const usesTessie = option.dataset.usesTessie === 'true'; const tessieOdometer = option.dataset.tessieOdometer; const hint = document.getElementById('last-odometer-hint'); const input = document.getElementById('odometer'); const badge = document.getElementById('tessie-badge'); + const unitBadge = document.getElementById('odometer-unit-badge'); + + // Update odometer unit display + if (unitBadge) unitBadge.textContent = odometerUnit; if (usesTessie && tessieOdometer) { // Show Tessie badge and auto-fill @@ -140,7 +146,7 @@

{{ _('Quick Fuel En input.classList.remove('bg-gray-100', 'dark:bg-gray-600'); if (odometer && parseFloat(odometer) > 0) { - hint.textContent = 'Last: ' + parseFloat(odometer).toLocaleString() + ' {{ current_user.distance_unit }}'; + hint.textContent = 'Last: ' + parseFloat(odometer).toLocaleString() + ' ' + odometerUnit; input.placeholder = odometer; } else { hint.textContent = ''; diff --git a/app/templates/vehicles/form.html b/app/templates/vehicles/form.html index c7f0755..fd4d71c 100644 --- a/app/templates/vehicles/form.html +++ b/app/templates/vehicles/form.html @@ -43,6 +43,18 @@

Basic Informa

Use "Hours" for tractors, ATVs, boats, etc.

+
+ + +

Override the odometer unit for this vehicle. Useful when you have vehicles with different odometer units.

+
+