Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion app/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
2 changes: 1 addition & 1 deletion app/routes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 12 additions & 2 deletions app/routes/stations.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,21 @@ def price_history(station_id):
station = FuelStation.query.get_or_404(station_id)

# Get price history ordered by date
prices = FuelPriceHistory.query.filter_by(station_id=station_id).order_by(
price_records = FuelPriceHistory.query.filter_by(station_id=station_id).order_by(
FuelPriceHistory.date.desc()
).limit(50).all()

return render_template('stations/prices.html', station=station, prices=prices)
prices = price_records
prices_json = [
{
'date': p.date.isoformat() if p.date else None,
'fuel_type': p.fuel_type,
'price_per_unit': p.price_per_unit,
}
for p in price_records
]

return render_template('stations/prices.html', station=station, prices=prices, prices_json=prices_json)


@bp.route('/cheapest')
Expand Down
10 changes: 7 additions & 3 deletions app/routes/vehicles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 11 additions & 5 deletions app/templates/fuel/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
{% for vehicle in vehicles %}
<option value="{{ vehicle.id }}"
{% if (log and log.vehicle_id == vehicle.id) or (selected_vehicle_id == vehicle.id) %}selected{% endif %}
data-last-odometer="{{ vehicle.get_last_odometer(current_user.distance_unit) }}"
data-last-odometer="{{ vehicle.get_last_odometer(vehicle.get_effective_odometer_unit()) }}"
data-odometer-unit="{{ vehicle.get_effective_odometer_unit() }}"
data-uses-tessie="{{ 'true' if vehicle.uses_tessie_odometer() else 'false' }}"
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 %}">
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 %}">
{{ vehicle.name }}
</option>
{% endfor %}
</select>
<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>
<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>
</div>

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

<div>
<label for="odometer" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Odometer ({{ current_user.distance_unit }}) *
Odometer (<span id="odometer-unit-label">{{ current_user.distance_unit }}</span>) *
<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">
<svg class="w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
Expand Down Expand Up @@ -164,9 +165,14 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
const usesTessie = selectedOption.getAttribute('data-uses-tessie') === 'true';
const tessieOdometer = selectedOption.getAttribute('data-tessie-odometer');
const lastOdometer = selectedOption.getAttribute('data-last-odometer');
const odometerUnit = selectedOption.getAttribute('data-odometer-unit') || '{{ current_user.distance_unit }}';

// Update last odometer display
// Update last odometer display and unit labels
document.getElementById('last-odometer').textContent = lastOdometer || 0;
const unitDisplay = document.getElementById('odometer-unit-display');
const unitLabel = document.getElementById('odometer-unit-label');
if (unitDisplay) unitDisplay.textContent = odometerUnit;
if (unitLabel) unitLabel.textContent = odometerUnit;

if (usesTessie && tessieOdometer) {
// Show Tessie badge and make field read-only
Expand Down
2 changes: 1 addition & 1 deletion app/templates/fuel/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Fuel Logs</h1>
{{ log.vehicle.name }}
</a>
</td>
<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>
<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>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ "%.1f"|format(log.volume or 0) }} {{ current_user.volume_unit }}
{% if log.is_full_tank %}
Expand Down
14 changes: 10 additions & 4 deletions app/templates/fuel/quick.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
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">
{% for vehicle in vehicles %}
<option value="{{ vehicle.id }}"
data-odometer="{{ vehicle.get_last_odometer(current_user.distance_unit) }}"
data-odometer="{{ vehicle.get_last_odometer(vehicle.get_effective_odometer_unit()) }}"
data-odometer-unit="{{ vehicle.get_effective_odometer_unit() }}"
data-uses-tessie="{{ 'true' if vehicle.uses_tessie_odometer() else 'false' }}"
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 %}"
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 %}"
{% if selected_vehicle_id and selected_vehicle_id|int == vehicle.id %}selected{% endif %}>
{{ vehicle.name }}
</option>
Expand All @@ -42,7 +43,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('Quick Fuel En
<input type="number" name="odometer" id="odometer" required step="0.1"
placeholder="{{ last_odometer or '0' }}"
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">
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{{ current_user.distance_unit }}</span>
<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>
</div>
<p id="last-odometer-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{% if last_odometer %}{{ _('Last') }}: {{ "{:,.0f}".format(last_odometer) }} {{ current_user.distance_unit }}{% endif %}
Expand Down Expand Up @@ -120,11 +121,16 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('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
Expand All @@ -140,7 +146,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ _('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 = '';
Expand Down
2 changes: 1 addition & 1 deletion app/templates/stations/prices.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white">{{ _('Price Histor
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('priceChart').getContext('2d');
const prices = {{ prices | tojson }};
const prices = {{ prices_json | tojson }};

// Reverse to show chronological order
const sortedPrices = prices.slice().reverse();
Expand Down
12 changes: 12 additions & 0 deletions app/templates/vehicles/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Basic Informa
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Use "Hours" for tractors, ATVs, boats, etc.</p>
</div>

<div>
<label for="odometer_unit" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Odometer Unit</label>
<select name="odometer_unit" id="odometer_unit"
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">
<option value="" {% if not vehicle or not vehicle.odometer_unit %}selected{% endif %}>Use account default ({{ current_user.distance_unit }})</option>
{% for value, label in odometer_units %}
<option value="{{ value }}" {% if vehicle and vehicle.odometer_unit == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<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>
</div>

<div>
<label for="fuel_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Fuel Type</label>
<select name="fuel_type" id="fuel_type"
Expand Down
Loading