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
10 changes: 10 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class Vehicle(db.Model):

# Fuel info
fuel_type = db.Column(db.String(20), default='petrol') # petrol, diesel, electric, hybrid, lpg
secondary_fuel_type = db.Column(db.String(20), nullable=True) # e.g. adblue, lpg
tank_capacity = db.Column(db.Float) # in liters
battery_capacity = db.Column(db.Float) # in kWh for EVs

Expand Down Expand Up @@ -245,6 +246,10 @@ def get_total_expense_cost(self):
def get_total_cost(self):
return self.get_total_fuel_cost() + self.get_total_expense_cost()

@property
def vehicle_type_label(self):
return dict(VEHICLE_TYPES).get(self.vehicle_type, self.vehicle_type.replace('_', ' ').title())

@property
def currency_symbol(self):
return get_currency_symbol(self.owner.currency if self.owner else None)
Expand Down Expand Up @@ -365,6 +370,7 @@ def to_dict(self):
'registration': self.registration,
'vin': self.vin,
'fuel_type': self.fuel_type,
'secondary_fuel_type': self.secondary_fuel_type,
'tank_capacity': self.tank_capacity,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
Expand Down Expand Up @@ -415,6 +421,7 @@ class FuelLog(db.Model):
price_per_unit = db.Column(db.Float) # price per liter
total_cost = db.Column(db.Float)

fuel_type = db.Column(db.String(20), nullable=True) # overrides vehicle primary; set when vehicle has secondary fuel type
is_full_tank = db.Column(db.Boolean, default=True)
is_missed = db.Column(db.Boolean, default=False) # missed fill-up flag

Expand Down Expand Up @@ -709,6 +716,9 @@ def get_all_branding():
('scooter', _l('Scooter')),
('truck', _l('Truck')),
('suv', _l('SUV')),
('hatchback', _l('Hatchback')),
('station_wagon', _l('Station Wagon / Estate')),
('pickup', _l('Pickup / Ute')),
('tractor', _l('Tractor')),
('atv_utv', _l('ATV/UTV')),
('boat', _l('Boat')),
Expand Down
10 changes: 7 additions & 3 deletions app/routes/fuel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from app import db
from app.models import Vehicle, FuelLog, Attachment, FuelStation, FuelPriceHistory
from app.models import Vehicle, FuelLog, Attachment, FuelStation, FuelPriceHistory, FUEL_TYPES
from app.security import validate_file_upload, secure_filename_with_uuid, validate_positive_number
from flask_babel import gettext as _
from app.services.tessie import TessieService
Expand Down Expand Up @@ -84,6 +84,7 @@ def new():
volume=volume,
price_per_unit=price_per_unit,
total_cost=total_cost,
fuel_type=request.form.get('fuel_type') or None,
is_full_tank=request.form.get('is_full_tank') == 'on',
is_missed=request.form.get('is_missed') == 'on',
station=request.form.get('station'),
Expand All @@ -107,7 +108,7 @@ def new():
station_id=station_id,
user_id=current_user.id,
date=log.date,
fuel_type=vehicle.fuel_type or 'petrol',
fuel_type=log.fuel_type or vehicle.fuel_type or 'petrol',
price_per_unit=log.price_per_unit
)
db.session.add(price_history)
Expand Down Expand Up @@ -146,6 +147,7 @@ def new():
log=None,
vehicles=vehicles,
stations=stations,
fuel_types=FUEL_TYPES,
selected_vehicle_id=selected_vehicle_id)


Expand Down Expand Up @@ -176,6 +178,7 @@ def edit(log_id):
log.volume = float(request.form.get('volume')) if request.form.get('volume') else None
log.price_per_unit = float(request.form.get('price_per_unit')) if request.form.get('price_per_unit') else None
log.total_cost = float(request.form.get('total_cost')) if request.form.get('total_cost') else None
log.fuel_type = request.form.get('fuel_type') or None
log.is_full_tank = request.form.get('is_full_tank') == 'on'
log.is_missed = request.form.get('is_missed') == 'on'
log.station = request.form.get('station')
Expand Down Expand Up @@ -208,7 +211,7 @@ def edit(log_id):
station_id=station_id,
user_id=current_user.id,
date=log.date,
fuel_type=log.vehicle.fuel_type or 'petrol',
fuel_type=log.fuel_type or log.vehicle.fuel_type or 'petrol',
price_per_unit=log.price_per_unit
))

Expand Down Expand Up @@ -241,6 +244,7 @@ def edit(log_id):
log=log,
vehicles=vehicles,
stations=stations,
fuel_types=FUEL_TYPES,
selected_vehicle_id=log.vehicle_id)


Expand Down
2 changes: 2 additions & 0 deletions app/routes/vehicles.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def new():
registration=request.form.get('registration'),
vin=request.form.get('vin'),
fuel_type=request.form.get('fuel_type'),
secondary_fuel_type=request.form.get('secondary_fuel_type') or None,
tank_capacity=float(request.form.get('tank_capacity')) if request.form.get('tank_capacity') else None,
notes=request.form.get('notes')
)
Expand Down Expand Up @@ -178,6 +179,7 @@ def edit(vehicle_id):
vehicle.registration = request.form.get('registration')
vehicle.vin = request.form.get('vin')
vehicle.fuel_type = request.form.get('fuel_type')
vehicle.secondary_fuel_type = request.form.get('secondary_fuel_type') or None
vehicle.tank_capacity = float(request.form.get('tank_capacity')) if request.form.get('tank_capacity') else None
vehicle.notes = request.form.get('notes')

Expand Down
2 changes: 1 addition & 1 deletion app/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white">{{ _('Your Vehicle
<li class="py-3">
<a href="{{ url_for('vehicles.view', vehicle_id=vehicle.id) }}" class="flex items-center hover:bg-gray-50 dark:bg-gray-700 -mx-2 px-2 rounded">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
{% if vehicle.vehicle_type == 'car' %}
{% if vehicle.vehicle_type in ('car', 'hatchback', 'station_wagon') %}
<svg class="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h8m-8 4h8m-4-8v16M5 17h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg>
{% elif vehicle.vehicle_type == 'motorbike' %}
<svg class="h-5 w-5 text-gray-500 dark:text-gray-400" 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"/></svg>
Expand Down
43 changes: 41 additions & 2 deletions app/templates/fuel/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
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 vehicle.get_effective_odometer_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 %}"
data-primary-fuel="{{ vehicle.fuel_type }}"
data-secondary-fuel="{{ vehicle.secondary_fuel_type or '' }}">
{{ vehicle.name }}
</option>
{% endfor %}
Expand Down Expand Up @@ -104,6 +106,13 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
{% endif %}
</div>

<div id="fuel-type-field" class="hidden">
<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"
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">
</select>
</div>

<div class="sm:col-span-2 flex flex-wrap gap-6">
<div class="flex items-center">
<input type="checkbox" name="is_full_tank" id="is_full_tank"
Expand Down Expand Up @@ -198,6 +207,31 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
}
}

function updateFuelTypeSelector(selectedOption) {
const fuelTypeField = document.getElementById('fuel-type-field');
const fuelTypeSelect = document.getElementById('fuel_type');
const primaryFuel = selectedOption.getAttribute('data-primary-fuel') || '';
const secondaryFuel = selectedOption.getAttribute('data-secondary-fuel') || '';
const fuelLabels = {{ dict(fuel_types)|tojson }};

if (secondaryFuel) {
fuelTypeSelect.innerHTML = '';
[primaryFuel, secondaryFuel].forEach(function(value) {
const opt = document.createElement('option');
opt.value = value;
opt.textContent = fuelLabels[value] || value;
{% if log and log.fuel_type %}
if (value === '{{ log.fuel_type }}') opt.selected = true;
{% endif %}
fuelTypeSelect.appendChild(opt);
});
fuelTypeField.classList.remove('hidden');
} else {
fuelTypeField.classList.add('hidden');
fuelTypeSelect.innerHTML = '';
}
}

function calculateTotal() {
const volume = parseFloat(document.getElementById('volume').value) || 0;
const pricePerUnit = parseFloat(document.getElementById('price_per_unit').value) || 0;
Expand All @@ -214,10 +248,15 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">{% if log %}Ed
}
}

// Initialize odometer for selected vehicle
// Initialize odometer and fuel type for selected vehicle
document.addEventListener('DOMContentLoaded', function() {
const vehicleSelect = document.getElementById('vehicle_id');
updateVehicleOdometer(vehicleSelect.value);
const selectedOption = vehicleSelect.options[vehicleSelect.selectedIndex];
updateFuelTypeSelector(selectedOption);
vehicleSelect.addEventListener('change', function() {
updateFuelTypeSelector(this.options[this.selectedIndex]);
});
});
</script>
{% endblock %}
12 changes: 12 additions & 0 deletions app/templates/vehicles/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ <h2 class="text-lg font-medium text-gray-900 dark:text-white mb-4">{{ _('Basic I
</select>
</div>

<div>
<label for="secondary_fuel_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Secondary Fuel Type') }}</label>
<select name="secondary_fuel_type" id="secondary_fuel_type"
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="">{{ _('None') }}</option>
{% for value, label in fuel_types %}
<option value="{{ value }}" {% if vehicle and vehicle.secondary_fuel_type == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _('Optional. Use for vehicles with a secondary fluid to track, e.g. AdBlue alongside Diesel.') }}</p>
</div>

<div>
<label for="make" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Make') }}</label>
<input type="text" name="make" id="make"
Expand Down
6 changes: 3 additions & 3 deletions app/templates/vehicles/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{% if show_archived
</div>
{% else %}
<div class="h-40 bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center">
{% if vehicle.vehicle_type == 'car' %}
{% if vehicle.vehicle_type in ('car', 'hatchback', 'station_wagon') %}
<!-- Car icon -->
<svg class="h-20 w-20 text-primary-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 17a2 2 0 100-4 2 2 0 000 4zm8 0a2 2 0 100-4 2 2 0 000 4z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M4 15V9a2 2 0 012-2h1l1.5-3h7L17 7h1a2 2 0 012 2v6M4 15h1m14 0h1M10 15h4"/>
</svg>
{% elif vehicle.vehicle_type == 'van' %}
{% elif vehicle.vehicle_type in ('van', 'pickup') %}
<!-- Van/Truck icon -->
<svg class="h-20 w-20 text-primary-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 17a2 2 0 100-4 2 2 0 000 4zm10 0a2 2 0 100-4 2 2 0 000 4z"/>
Expand Down Expand Up @@ -85,7 +85,7 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ vehicle.name }}
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ vehicle.make or '' }} {{ vehicle.model or '' }} {% if vehicle.year %}({{ vehicle.year }}){% endif %}</p>
<div class="mt-4 flex items-center justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400 capitalize">{{ vehicle.vehicle_type }}</span>
<span class="text-gray-500 dark:text-gray-400">{{ vehicle.vehicle_type_label }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{ "%.2f"|format(vehicle.get_total_cost()) }} {{ current_user.currency }}</span>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/vehicles/report_pdf.html
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ <h2>Vehicle Details</h2>
</tr>
<tr>
<td>Type</td>
<td style="text-transform: capitalize;">{{ vehicle.vehicle_type }}</td>
<td>{{ vehicle.vehicle_type_label }}</td>
</tr>
<tr>
<td>Make</td>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/vehicles/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ vehicle.name }}<
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ _('Type') }}</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white capitalize">{{ vehicle.vehicle_type }}</dd>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ vehicle.vehicle_type_label }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ _('Fuel') }}</dt>
Expand Down
2 changes: 1 addition & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
basedir = Path(__file__).parent.absolute()


APP_VERSION = '0.20.0'
APP_VERSION = '0.21.0'
RELEASE_CHANNEL = os.environ.get('RELEASE_CHANNEL', 'stable')
GIT_SHA = os.environ.get('GIT_SHA', '')[:7] # Short SHA
GITHUB_REPO = 'dannymcc/may'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add secondary_fuel_type to vehicles and fuel_type to fuel_logs

Revision ID: ee92897cc33b
Revises: b2c3d4e5f6a7
Create Date: 2026-04-22 21:28:46.208340

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'ee92897cc33b'
down_revision = 'b2c3d4e5f6a7'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table('fuel_logs', schema=None) as batch_op:
batch_op.add_column(sa.Column('fuel_type', sa.String(length=20), nullable=True))

with op.batch_alter_table('vehicles', schema=None) as batch_op:
batch_op.add_column(sa.Column('secondary_fuel_type', sa.String(length=20), nullable=True))


def downgrade():
with op.batch_alter_table('vehicles', schema=None) as batch_op:
batch_op.drop_column('secondary_fuel_type')

with op.batch_alter_table('fuel_logs', schema=None) as batch_op:
batch_op.drop_column('fuel_type')
Loading