diff --git a/app/models.py b/app/models.py
index db928b3..910620d 100644
--- a/app/models.py
+++ b/app/models.py
@@ -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
@@ -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)
@@ -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,
@@ -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
@@ -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')),
diff --git a/app/routes/fuel.py b/app/routes/fuel.py
index 03977c0..742ac45 100644
--- a/app/routes/fuel.py
+++ b/app/routes/fuel.py
@@ -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
@@ -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'),
@@ -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)
@@ -146,6 +147,7 @@ def new():
log=None,
vehicles=vehicles,
stations=stations,
+ fuel_types=FUEL_TYPES,
selected_vehicle_id=selected_vehicle_id)
@@ -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')
@@ -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
))
@@ -241,6 +244,7 @@ def edit(log_id):
log=log,
vehicles=vehicles,
stations=stations,
+ fuel_types=FUEL_TYPES,
selected_vehicle_id=log.vehicle_id)
diff --git a/app/routes/vehicles.py b/app/routes/vehicles.py
index 1a929b5..a4762ed 100644
--- a/app/routes/vehicles.py
+++ b/app/routes/vehicles.py
@@ -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')
)
@@ -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')
diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html
index 98f2c99..5f15e4f 100644
--- a/app/templates/dashboard.html
+++ b/app/templates/dashboard.html
@@ -162,7 +162,7 @@
{{ _('Your Vehicle
- {% if vehicle.vehicle_type == 'car' %}
+ {% if vehicle.vehicle_type in ('car', 'hatchback', 'station_wagon') %}
{% elif vehicle.vehicle_type == 'motorbike' %}
diff --git a/app/templates/fuel/form.html b/app/templates/fuel/form.html
index 03e3935..726cbd1 100644
--- a/app/templates/fuel/form.html
+++ b/app/templates/fuel/form.html
@@ -23,7 +23,9 @@
{% 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 }}
{% endfor %}
@@ -104,6 +106,13 @@ {% if log %}Ed
{% endif %}
+
+
+
+
+
{% 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;
@@ -214,10 +248,15 @@
{% 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]);
+ });
});
{% endblock %}
diff --git a/app/templates/vehicles/form.html b/app/templates/vehicles/form.html
index 76f231b..4ca99bf 100644
--- a/app/templates/vehicles/form.html
+++ b/app/templates/vehicles/form.html
@@ -65,6 +65,18 @@ {{ _('Basic I
+
+
+
+
{{ _('Optional. Use for vehicles with a secondary fluid to track, e.g. AdBlue alongside Diesel.') }}
+
+
{% if show_archived
{% else %}
- {% if vehicle.vehicle_type == 'car' %}
+ {% if vehicle.vehicle_type in ('car', 'hatchback', 'station_wagon') %}
- {% elif vehicle.vehicle_type == 'van' %}
+ {% elif vehicle.vehicle_type in ('van', 'pickup') %}
{{ vehicle.make or '' }} {{ vehicle.model or '' }} {% if vehicle.year %}({{ vehicle.year }}){% endif %}
- {{ vehicle.vehicle_type }}
+ {{ vehicle.vehicle_type_label }}
{{ "%.2f"|format(vehicle.get_total_cost()) }} {{ current_user.currency }}
diff --git a/app/templates/vehicles/report_pdf.html b/app/templates/vehicles/report_pdf.html
index 0ff3954..7143fe3 100644
--- a/app/templates/vehicles/report_pdf.html
+++ b/app/templates/vehicles/report_pdf.html
@@ -241,7 +241,7 @@ Vehicle Details
| Type |
- {{ vehicle.vehicle_type }} |
+ {{ vehicle.vehicle_type_label }} |
| Make |
diff --git a/app/templates/vehicles/view.html b/app/templates/vehicles/view.html
index e95adae..f032f85 100644
--- a/app/templates/vehicles/view.html
+++ b/app/templates/vehicles/view.html
@@ -49,7 +49,7 @@ {{ vehicle.name }}<
{{ _('Type') }}
- {{ vehicle.vehicle_type }}
+ {{ vehicle.vehicle_type_label }}
{{ _('Fuel') }}
diff --git a/config.py b/config.py
index 7eda3f7..bfd6dcb 100644
--- a/config.py
+++ b/config.py
@@ -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'
diff --git a/migrations/versions/ee92897cc33b_add_secondary_fuel_type_to_vehicles_and_.py b/migrations/versions/ee92897cc33b_add_secondary_fuel_type_to_vehicles_and_.py
new file mode 100644
index 0000000..266a652
--- /dev/null
+++ b/migrations/versions/ee92897cc33b_add_secondary_fuel_type_to_vehicles_and_.py
@@ -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')