Skip to content

Commit 3f28298

Browse files
authored
Merge pull request #80 from dannymcc/dev
v0.13.0 — Per-vehicle odometer units & price history fix
2 parents a57467f + aa1bd7e commit 3f28298

13 files changed

Lines changed: 109 additions & 24 deletions

File tree

app/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ class Vehicle(db.Model):
174174
# Tracking unit (mileage or hours)
175175
tracking_unit = db.Column(db.String(20), default='mileage') # mileage, hours
176176

177+
# Per-vehicle odometer unit override (if None, falls back to user's distance_unit)
178+
odometer_unit = db.Column(db.String(10), default=None) # km, mi, or None (use user preference)
179+
177180
# Fuel info
178181
fuel_type = db.Column(db.String(20), default='petrol') # petrol, diesel, electric, hybrid, lpg
179182
tank_capacity = db.Column(db.Float) # in liters
@@ -219,6 +222,18 @@ class Vehicle(db.Model):
219222
charging_sessions = db.relationship('ChargingSession', backref='vehicle', lazy='dynamic',
220223
cascade='all, delete-orphan')
221224

225+
def get_effective_odometer_unit(self):
226+
"""Return the odometer unit for this vehicle.
227+
228+
Uses the vehicle's own odometer_unit if set, otherwise falls back to
229+
the owner's distance_unit preference.
230+
"""
231+
if self.odometer_unit:
232+
return self.odometer_unit
233+
if self.owner:
234+
return self.owner.distance_unit
235+
return 'km'
236+
222237
def get_total_fuel_cost(self):
223238
return sum(log.total_cost for log in self.fuel_logs.all() if log.total_cost)
224239

@@ -644,6 +659,12 @@ def get_all_branding():
644659
('hours', 'Hours'),
645660
]
646661

662+
# Odometer unit options (for per-vehicle override)
663+
ODOMETER_UNITS = [
664+
('km', 'Kilometres (km)'),
665+
('mi', 'Miles (mi)'),
666+
]
667+
647668
# Fuel types
648669
FUEL_TYPES = [
649670
('petrol', 'Petrol/Gasoline'),

app/routes/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def vehicle_stats(vehicle_id):
295295
'expenses_by_category': category_totals,
296296
'total_fuel_cost': vehicle.get_total_fuel_cost(),
297297
'total_expense_cost': vehicle.get_total_expense_cost(),
298-
'total_distance': vehicle.get_total_distance(current_user.distance_unit),
298+
'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()),
299299
'avg_consumption': vehicle.get_average_consumption()
300300
})
301301

app/routes/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def dashboard():
7373
).scalar()
7474
total_expense_cost = expense_result or 0
7575

76-
# Total distance
76+
# Total distance (uses user's distance_unit for Tessie conversion; raw logs are in their stored unit)
7777
for vehicle in vehicles:
7878
total_distance += vehicle.get_total_distance(current_user.distance_unit)
7979

app/routes/stations.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,21 @@ def price_history(station_id):
134134
station = FuelStation.query.get_or_404(station_id)
135135

136136
# Get price history ordered by date
137-
prices = FuelPriceHistory.query.filter_by(station_id=station_id).order_by(
137+
price_records = FuelPriceHistory.query.filter_by(station_id=station_id).order_by(
138138
FuelPriceHistory.date.desc()
139139
).limit(50).all()
140140

141-
return render_template('stations/prices.html', station=station, prices=prices)
141+
prices = price_records
142+
prices_json = [
143+
{
144+
'date': p.date.isoformat() if p.date else None,
145+
'fuel_type': p.fuel_type,
146+
'price_per_unit': p.price_per_unit,
147+
}
148+
for p in price_records
149+
]
150+
151+
return render_template('stations/prices.html', station=station, prices=prices, prices_json=prices_json)
142152

143153

144154
@bp.route('/cheapest')

app/routes/vehicles.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from flask_babel import gettext as _
88
from werkzeug.utils import secure_filename
99
from app import db
10-
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
10+
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
1111
from app.services.tessie import TessieService
1212

1313
bp = Blueprint('vehicles', __name__, url_prefix='/vehicles')
@@ -47,6 +47,7 @@ def new():
4747
name=request.form.get('name'),
4848
vehicle_type=request.form.get('vehicle_type'),
4949
tracking_unit=request.form.get('tracking_unit', 'mileage'),
50+
odometer_unit=request.form.get('odometer_unit') or None,
5051
make=request.form.get('make'),
5152
model=request.form.get('model'),
5253
year=int(request.form.get('year')) if request.form.get('year') else None,
@@ -95,6 +96,7 @@ def new():
9596
vehicle_types=VEHICLE_TYPES,
9697
fuel_types=FUEL_TYPES,
9798
tracking_units=TRACKING_UNITS,
99+
odometer_units=ODOMETER_UNITS,
98100
spec_types=VEHICLE_SPEC_TYPES,
99101
tessie_configured=tessie_configured)
100102

@@ -121,7 +123,7 @@ def view(vehicle_id):
121123
'total_fuel_cost': vehicle.get_total_fuel_cost(),
122124
'total_expense_cost': vehicle.get_total_expense_cost(),
123125
'total_cost': vehicle.get_total_cost(),
124-
'total_distance': vehicle.get_total_distance(current_user.distance_unit),
126+
'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()),
125127
'avg_consumption': vehicle.get_average_consumption(),
126128
'fuel_logs_count': vehicle.fuel_logs.count(),
127129
'expenses_count': vehicle.expenses.count()
@@ -168,6 +170,7 @@ def edit(vehicle_id):
168170
vehicle.name = request.form.get('name')
169171
vehicle.vehicle_type = request.form.get('vehicle_type')
170172
vehicle.tracking_unit = request.form.get('tracking_unit', 'mileage')
173+
vehicle.odometer_unit = request.form.get('odometer_unit') or None
171174
vehicle.make = request.form.get('make')
172175
vehicle.model = request.form.get('model')
173176
vehicle.year = int(request.form.get('year')) if request.form.get('year') else None
@@ -224,6 +227,7 @@ def edit(vehicle_id):
224227
vehicle_types=VEHICLE_TYPES,
225228
fuel_types=FUEL_TYPES,
226229
tracking_units=TRACKING_UNITS,
230+
odometer_units=ODOMETER_UNITS,
227231
spec_types=VEHICLE_SPEC_TYPES,
228232
specs=specs,
229233
tessie_configured=tessie_configured)
@@ -360,7 +364,7 @@ def report(vehicle_id):
360364
'total_fuel_cost': vehicle.get_total_fuel_cost(),
361365
'total_expense_cost': vehicle.get_total_expense_cost(),
362366
'total_cost': vehicle.get_total_cost(),
363-
'total_distance': vehicle.get_total_distance(current_user.distance_unit),
367+
'total_distance': vehicle.get_total_distance(vehicle.get_effective_odometer_unit()),
364368
'avg_consumption': vehicle.get_average_consumption(),
365369
'fuel_logs_count': len(fuel_logs),
366370
'expenses_count': len(expenses)

app/templates/fuel/form.html

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
2020
{% for vehicle in vehicles %}
2121
<option value="{{ vehicle.id }}"
2222
{% if (log and log.vehicle_id == vehicle.id) or (selected_vehicle_id == vehicle.id) %}selected{% endif %}
23-
data-last-odometer="{{ vehicle.get_last_odometer(current_user.distance_unit) }}"
23+
data-last-odometer="{{ vehicle.get_last_odometer(vehicle.get_effective_odometer_unit()) }}"
24+
data-odometer-unit="{{ vehicle.get_effective_odometer_unit() }}"
2425
data-uses-tessie="{{ 'true' if vehicle.uses_tessie_odometer() else 'false' }}"
25-
data-tessie-odometer="{% if vehicle.tessie_last_odometer %}{% if current_user.distance_unit == 'mi' %}{{ (vehicle.tessie_last_odometer * 0.621371)|round|int }}{% else %}{{ vehicle.tessie_last_odometer|round|int }}{% endif %}{% endif %}">
26+
data-tessie-odometer="{% if vehicle.tessie_last_odometer %}{% if vehicle.get_effective_odometer_unit() == 'mi' %}{{ (vehicle.tessie_last_odometer * 0.621371)|round|int }}{% else %}{{ vehicle.tessie_last_odometer|round|int }}{% endif %}{% endif %}">
2627
{{ vehicle.name }}
2728
</option>
2829
{% endfor %}
2930
</select>
30-
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Last odometer: <span id="last-odometer">-</span> {{ current_user.distance_unit }}</p>
31+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Last odometer: <span id="last-odometer">-</span> <span id="odometer-unit-display">{{ current_user.distance_unit }}</span></p>
3132
</div>
3233

3334
<div>
@@ -39,7 +40,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
3940

4041
<div>
4142
<label for="odometer" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
42-
Odometer ({{ current_user.distance_unit }}) *
43+
Odometer (<span id="odometer-unit-label">{{ current_user.distance_unit }}</span>) *
4344
<span id="tessie-odometer-badge" class="hidden ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
4445
<svg class="w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4546
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
@@ -164,9 +165,14 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
164165
const usesTessie = selectedOption.getAttribute('data-uses-tessie') === 'true';
165166
const tessieOdometer = selectedOption.getAttribute('data-tessie-odometer');
166167
const lastOdometer = selectedOption.getAttribute('data-last-odometer');
168+
const odometerUnit = selectedOption.getAttribute('data-odometer-unit') || '{{ current_user.distance_unit }}';
167169

168-
// Update last odometer display
170+
// Update last odometer display and unit labels
169171
document.getElementById('last-odometer').textContent = lastOdometer || 0;
172+
const unitDisplay = document.getElementById('odometer-unit-display');
173+
const unitLabel = document.getElementById('odometer-unit-label');
174+
if (unitDisplay) unitDisplay.textContent = odometerUnit;
175+
if (unitLabel) unitLabel.textContent = odometerUnit;
170176

171177
if (usesTessie && tessieOdometer) {
172178
// Show Tessie badge and make field read-only

app/templates/fuel/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Fuel Logs</h1>
4040
{{ log.vehicle.name }}
4141
</a>
4242
</td>
43-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ "%.0f"|format(log.odometer) }} {{ current_user.distance_unit }}</td>
43+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ "%.0f"|format(log.odometer) }} {{ log.vehicle.get_effective_odometer_unit() }}</td>
4444
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
4545
{{ "%.1f"|format(log.volume or 0) }} {{ current_user.volume_unit }}
4646
{% if log.is_full_tank %}

app/templates/fuel/quick.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
1717
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-lg py-3 focus:border-primary-500 focus:ring-primary-500">
1818
{% for vehicle in vehicles %}
1919
<option value="{{ vehicle.id }}"
20-
data-odometer="{{ vehicle.get_last_odometer(current_user.distance_unit) }}"
20+
data-odometer="{{ vehicle.get_last_odometer(vehicle.get_effective_odometer_unit()) }}"
21+
data-odometer-unit="{{ vehicle.get_effective_odometer_unit() }}"
2122
data-uses-tessie="{{ 'true' if vehicle.uses_tessie_odometer() else 'false' }}"
22-
data-tessie-odometer="{% if vehicle.tessie_last_odometer %}{% if current_user.distance_unit == 'mi' %}{{ (vehicle.tessie_last_odometer * 0.621371)|round|int }}{% else %}{{ vehicle.tessie_last_odometer|round|int }}{% endif %}{% endif %}"
23+
data-tessie-odometer="{% if vehicle.tessie_last_odometer %}{% if vehicle.get_effective_odometer_unit() == 'mi' %}{{ (vehicle.tessie_last_odometer * 0.621371)|round|int }}{% else %}{{ vehicle.tessie_last_odometer|round|int }}{% endif %}{% endif %}"
2324
{% if selected_vehicle_id and selected_vehicle_id|int == vehicle.id %}selected{% endif %}>
2425
{{ vehicle.name }}
2526
</option>
@@ -42,7 +43,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
4243
<input type="number" name="odometer" id="odometer" required step="0.1"
4344
placeholder="{{ last_odometer or '0' }}"
4445
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-lg py-3 pr-12 focus:border-primary-500 focus:ring-primary-500">
45-
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{{ current_user.distance_unit }}</span>
46+
<span id="odometer-unit-badge" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{{ current_user.distance_unit }}</span>
4647
</div>
4748
<p id="last-odometer-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
4849
{% if last_odometer %}{{ _('Last') }}: {{ "{:,.0f}".format(last_odometer) }} {{ current_user.distance_unit }}{% endif %}
@@ -120,11 +121,16 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
120121
const select = document.getElementById('vehicle_id');
121122
const option = select.options[select.selectedIndex];
122123
const odometer = option.dataset.odometer;
124+
const odometerUnit = option.dataset.odometerUnit || '{{ current_user.distance_unit }}';
123125
const usesTessie = option.dataset.usesTessie === 'true';
124126
const tessieOdometer = option.dataset.tessieOdometer;
125127
const hint = document.getElementById('last-odometer-hint');
126128
const input = document.getElementById('odometer');
127129
const badge = document.getElementById('tessie-badge');
130+
const unitBadge = document.getElementById('odometer-unit-badge');
131+
132+
// Update odometer unit display
133+
if (unitBadge) unitBadge.textContent = odometerUnit;
128134

129135
if (usesTessie && tessieOdometer) {
130136
// Show Tessie badge and auto-fill
@@ -140,7 +146,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
140146
input.classList.remove('bg-gray-100', 'dark:bg-gray-600');
141147

142148
if (odometer && parseFloat(odometer) > 0) {
143-
hint.textContent = 'Last: ' + parseFloat(odometer).toLocaleString() + ' {{ current_user.distance_unit }}';
149+
hint.textContent = 'Last: ' + parseFloat(odometer).toLocaleString() + ' ' + odometerUnit;
144150
input.placeholder = odometer;
145151
} else {
146152
hint.textContent = '';

app/templates/stations/prices.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white">{{ _('Price Histor
5353
<script>
5454
document.addEventListener('DOMContentLoaded', function() {
5555
const ctx = document.getElementById('priceChart').getContext('2d');
56-
const prices = {{ prices | tojson }};
56+
const prices = {{ prices_json | tojson }};
5757

5858
// Reverse to show chronological order
5959
const sortedPrices = prices.slice().reverse();

app/templates/vehicles/form.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Basic Informa
4343
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Use "Hours" for tractors, ATVs, boats, etc.</p>
4444
</div>
4545

46+
<div>
47+
<label for="odometer_unit" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Odometer Unit</label>
48+
<select name="odometer_unit" id="odometer_unit"
49+
class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
50+
<option value="" {% if not vehicle or not vehicle.odometer_unit %}selected{% endif %}>Use account default ({{ current_user.distance_unit }})</option>
51+
{% for value, label in odometer_units %}
52+
<option value="{{ value }}" {% if vehicle and vehicle.odometer_unit == value %}selected{% endif %}>{{ label }}</option>
53+
{% endfor %}
54+
</select>
55+
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Override the odometer unit for this vehicle. Useful when you have vehicles with different odometer units.</p>
56+
</div>
57+
4658
<div>
4759
<label for="fuel_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Fuel Type</label>
4860
<select name="fuel_type" id="fuel_type"

0 commit comments

Comments
 (0)