From c0a8b748be0adf2cdc1da3683d31840cb8a0c4e6 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Tue, 4 Aug 2026 17:29:50 +0200 Subject: [PATCH 1/3] Adds fuel level tracking to trips --- app/models.py | 50 +++++++++++++++++++ app/routes/api.py | 18 +++++-- app/routes/trips.py | 2 + app/templates/trips/form.html | 30 +++++++++++ app/templates/trips/index.html | 10 +++- app/templates/vehicles/view.html | 6 ++- .../cc6e159f098a_adds_fuel_level_to_trip.py | 35 +++++++++++++ tests/conftest.py | 1 + tests/test_models.py | 30 +++++++++++ tests/test_trips.py | 10 +++- 10 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py diff --git a/app/models.py b/app/models.py index 51ee021..0762428 100644 --- a/app/models.py +++ b/app/models.py @@ -397,6 +397,13 @@ def uses_tessie_odometer(self): self.tessie_vin and TessieService.is_configured()) + def uses_tessie_battery(self): + """Check if this vehicle uses Tessie for battery tracking""" + from app.services.tessie import TessieService + return (self.tessie_enabled and + self.tessie_vin and + TessieService.is_configured()) + def get_last_odometer(self, distance_unit=None): """Get the most recent odometer reading. @@ -428,6 +435,26 @@ def get_last_odometer(self, distance_unit=None): return max(fuel_odo, trip_odo, charge_odo) + def get_last_fuel_level(self, distance_unit=None): + """Get the most recent fuel level. + + Returns the highest from trips or charging sessions. + """ + # If Tessie is enabled, use Tessie battery level exclusively + if self.uses_tessie_battery() and self.tessie_battery_level: + return round(self.tessie_battery_level) + + last_trip = self.trips.filter(Trip.end_fuel_level.isnot(None)).order_by(Trip.date.desc()).first() + + last_charge = self.charging_sessions.filter(ChargingSession.end_soc.isnot(None)).order_by( + ChargingSession.date.desc()).first() + + if not last_trip and not last_charge: return 0 + if not last_trip: return last_charge.end_soc + if not last_charge: return last_trip.end_fuel_level + + return last_trip.end_fuel_level if last_trip.date > last_charge.date else last_charge.end_soc + def get_total_charging_cost(self): """Get total cost of all charging sessions""" return sum(session.total_cost for session in self.charging_sessions.all() if session.total_cost) or 0 @@ -1344,6 +1371,8 @@ class Trip(db.Model): date = db.Column(db.Date, nullable=False, default=datetime.utcnow) start_odometer = db.Column(db.Float, nullable=False) end_odometer = db.Column(db.Float, nullable=True) + start_fuel_level = db.Column(db.Float, nullable=True) + end_fuel_level = db.Column(db.Float, nullable=True) purpose = db.Column(db.String(20), nullable=False) # business, personal, commute, etc. description = db.Column(db.String(200)) @@ -1363,6 +1392,25 @@ def distance(self): return 0 return self.end_odometer - self.start_odometer + @property + def fuel_consumption(self) -> float | None: + """Calculate trip fuel consumption""" + if self.end_fuel_level is None or self.start_fuel_level is None: + return None + return self.start_fuel_level - self.end_fuel_level + + @property + def fuel_consumption_human_readable(self) -> str | None: + """Calculate trip fuel consumption with a plus sign instead of negative for negative fuel consumption""" + consumption = self.fuel_consumption + if consumption is None: return None + if self.vehicle.tank_capacity: + abs_consumption = self.vehicle.tank_capacity * consumption / 100 + if abs_consumption < 0: return "~+{:.1f} L".format(abs(abs_consumption)) + return "~{:.1f} L".format(abs_consumption) + if consumption < 0: return '+' + str(abs(consumption)) + ' %' + return str(consumption) + ' %' + def to_dict(self): """Serialize trip to dictionary for API""" return { @@ -1371,6 +1419,8 @@ def to_dict(self): 'date': self.date.isoformat() if self.date else None, 'start_odometer': self.start_odometer, 'end_odometer': self.end_odometer, + 'start_fuel_level': self.start_fuel_level, + 'end_fuel_level': self.end_fuel_level, 'distance': self.distance, 'purpose': self.purpose, 'description': self.description, diff --git a/app/routes/api.py b/app/routes/api.py index d89b150..1886dc6 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -1273,8 +1273,8 @@ def export_csv(): trips_csv = io.StringIO() writer = csv.writer(trips_csv) writer.writerow([ - 'id', 'vehicle_id', 'vehicle_name', 'date', 'start_odometer', 'end_odometer', - 'distance', 'distance_unit', 'purpose', 'description', 'start_location', 'end_location', + 'id', 'vehicle_id', 'vehicle_name', 'date', 'start_odometer', 'end_odometer', 'start_fuel_level', + 'end_fuel_level', 'distance', 'distance_unit', 'purpose', 'description', 'start_location', 'end_location', 'notes', 'created_at' ]) for vehicle in current_user.get_all_vehicles(): @@ -1283,7 +1283,9 @@ def export_csv(): writer.writerow([ trip.id, vehicle.id, vehicle.name, trip.date.isoformat() if trip.date else '', - trip.start_odometer, trip.end_odometer, trip.distance, odometer_unit, + trip.start_odometer, trip.end_odometer, + trip.start_fuel_level, trip.end_fuel_level, + trip.distance, odometer_unit, trip.purpose, trip.description, trip.start_location, trip.end_location, trip.notes, @@ -1565,6 +1567,8 @@ def export_json(): 'date': trip.date.isoformat() if trip.date else None, 'start_odometer': trip.start_odometer, 'end_odometer': trip.end_odometer, + 'start_fuel_level': trip.start_fuel_level, + 'end_fuel_level': trip.end_fuel_level, 'distance': trip.distance, 'purpose': trip.purpose, 'description': trip.description, @@ -1890,6 +1894,8 @@ def export_full_backup(): 'date': trip.date.isoformat() if trip.date else None, 'start_odometer': trip.start_odometer, 'end_odometer': trip.end_odometer, + 'start_fuel_level': trip.start_fuel_level, + 'end_fuel_level': trip.end_fuel_level, 'distance': trip.distance, 'purpose': trip.purpose, 'description': trip.description, @@ -2694,6 +2700,8 @@ def get_import_fields(data_type): {'name': 'date', 'label': 'Date', 'required': True, 'type': 'date'}, {'name': 'start_odometer', 'label': 'Start Odometer', 'required': True, 'type': 'float'}, {'name': 'end_odometer', 'label': 'End Odometer', 'required': True, 'type': 'float'}, + {'name': 'start_fuel_level', 'label': 'Start Fuel Level', 'required': False, 'type': 'float'}, + {'name': 'end_fuel_level', 'label': 'End Fuel Level', 'required': False, 'type': 'float'}, {'name': 'purpose', 'label': 'Purpose', 'required': True, 'type': 'str'}, {'name': 'description', 'label': 'Description', 'required': False, 'type': 'str'}, {'name': 'start_location', 'label': 'Start Location', 'required': False, 'type': 'str'}, @@ -2735,6 +2743,8 @@ def get_import_fields(data_type): 'cost': ['cost', 'amount', 'total', 'price', 'expense'], 'vendor': ['vendor', 'shop', 'store', 'supplier', 'merchant', 'provider'], 'start_odometer': ['start odometer', 'start odo', 'start miles', 'start km', 'odometer start'], + 'end_fuel_level': ['end fuel level', 'end fuel', 'end gas', 'fuel level', 'fuel', 'end battery', 'battery'], + 'start_fuel_level': ['start fuel level', 'start fuel', 'start gas', 'start battery'], 'end_odometer': ['end odometer', 'end odo', 'end miles', 'end km', 'odometer end'], 'purpose': ['purpose', 'trip purpose', 'reason', 'trip type'], 'start_location': ['start location', 'from', 'origin', 'departure'], @@ -2957,6 +2967,8 @@ def create_record(data_type, mapped_row, vehicle_id, user_id, date_format, user_ date=date_val, start_odometer=start_odo, end_odometer=end_odo, + start_fuel_level=parse_float_value(mapped_row.get('start_fuel_level')), + end_fuel_level=parse_float_value(mapped_row.get('end_fuel_level')), purpose=purpose, description=mapped_row.get('description', '').strip() or None, start_location=mapped_row.get('start_location', '').strip() or None, diff --git a/app/routes/trips.py b/app/routes/trips.py index f2048bf..01a3329 100644 --- a/app/routes/trips.py +++ b/app/routes/trips.py @@ -144,6 +144,8 @@ def edit(trip_id): trip.date = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else trip.date trip.start_odometer = parse_decimal(request.form.get('start_odometer')) trip.end_odometer = parse_decimal(request.form.get('end_odometer')) if request.form.get('end_odometer') else None + trip.start_fuel_level = parse_decimal(request.form.get('start_fuel_level')) if request.form.get('start_fuel_level') else None + trip.end_fuel_level = parse_decimal(request.form.get('end_fuel_level')) if request.form.get('end_fuel_level') else None trip.purpose = request.form.get('purpose') trip.description = request.form.get('description') trip.start_location = request.form.get('start_location') diff --git a/app/templates/trips/form.html b/app/templates/trips/form.html index 579f7e0..adf688d 100644 --- a/app/templates/trips/form.html +++ b/app/templates/trips/form.html @@ -88,6 +88,23 @@

{% if trip %}{

{{ _('Distance') }}: 0 {{ current_user.distance_unit }}

+
+ + +
+ +
+ + +

{{ _('Consumption') }}: 0 %

+
+
{% if trip %}{ document.getElementById('trip-distance').textContent = distance > 0 ? distance.toFixed(1) : '0'; } +function calculateFuelConsumption() { + const start = parseFloat(document.getElementById('start_fuel_level').value); + const end = parseFloat(document.getElementById('end_fuel_level').value); + const consumption = start - end; + let textContent = '0'; + if (start && end) { + if (consumption > 0) textContent = consumption.toFixed(1); + else if (consumption < 0) textContent = '+' + Math.abs(consumption).toFixed(1); + } + document.getElementById('trip-fuel-consumption').textContent = textContent +} + document.addEventListener('DOMContentLoaded', function() { calculateDistance(); + calculateFuelConsumption(); const vehicleSelect = document.getElementById('vehicle_id'); updateVehicleOdometer(vehicleSelect.value); diff --git a/app/templates/trips/index.html b/app/templates/trips/index.html index 471e8d3..291cee9 100644 --- a/app/templates/trips/index.html +++ b/app/templates/trips/index.html @@ -117,8 +117,14 @@

{{ _('Trips') }}
-

{{ "%.1f"|format(trip.distance) }} {{ current_user.distance_unit }}

-

{{ "%.0f"|format(trip.start_odometer) }} → {{ "%.0f"|format(trip.end_odometer or 0) }}

+

+ {{ "%.1f"|format(trip.distance) }} {{ current_user.distance_unit }} + {% if trip.fuel_consumption %}· {{ trip.fuel_consumption_human_readable }}{% endif %} +

+

+ {{ "%.0f"|format(trip.start_odometer) }} → {{ "%.0f"|format(trip.end_odometer or 0) }} + {% if trip.fuel_consumption %}· {{ trip.start_fuel_level or "?" }} % → {{ trip.end_fuel_level or "?" }} %{% endif %} +

-
+
{{ _('Type') }}
{{ vehicle.vehicle_type_label }}
@@ -63,6 +63,10 @@

{{ vehicle.name }}<
{{ _('Last Odometer') }}
{{ "%.0f"|format(vehicle.get_last_odometer(vehicle.get_effective_odometer_unit())) }} {{ vehicle.get_effective_odometer_unit() }}

+
+
{{ _('Last Fuel Level') }}
+
{{ "%.0f"|format(vehicle.get_last_fuel_level())}} %
+

diff --git a/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py new file mode 100644 index 0000000..3966ed4 --- /dev/null +++ b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py @@ -0,0 +1,35 @@ +"""adds fuel level to trip and fuel logs + +Revision ID: cc6e159f098a +Revises: e5f6a7b8c9d0 +Create Date: 2026-08-03 21:46:57.015964 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cc6e159f098a' +down_revision = 'e5f6a7b8c9d0' +branch_labels = None +depends_on = None + + +def upgrade(): + bind = op.get_bind() + inspector = sa.inspect(bind) + if 'trips' in inspector.get_table_names(): + existing_cols_trip = [col['name'] for col in inspector.get_columns('trips')] + if 'start_fuel_level' not in existing_cols_trip or 'end_fuel_level' not in existing_cols_trip: + with op.batch_alter_table('trips', schema=None) as batch_op: + if 'start_fuel_level' not in existing_cols_trip: + batch_op.add_column(sa.Column('start_fuel_level', sa.Float(), nullable=True)) + if 'end_fuel_level' not in existing_cols_trip: + batch_op.add_column(sa.Column('end_fuel_level', sa.Float(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table('trips', schema=None) as batch_op: + batch_op.drop_column('start_fuel_level') + batch_op.drop_column('end_fuel_level') diff --git a/tests/conftest.py b/tests/conftest.py index 79b5a35..4b44c27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,6 +131,7 @@ def sample_vehicle(app, test_user): year=2023, fuel_type='petrol', odometer_unit='km', + tank_capacity=50, ) _db_ext.session.add(vehicle) _db_ext.session.commit() diff --git a/tests/test_models.py b/tests/test_models.py index 112c5ec..0400ac8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -669,6 +669,34 @@ def test_trip_distance_property(self, app, test_user, sample_vehicle): db.session.commit() assert trip.distance == 150.0 + def test_trip_fuel_consumption_property(self, app, test_user, sample_vehicle): + trip = Trip( + vehicle_id=sample_vehicle.id, + user_id=test_user.id, + date=date(2024, 1, 10), + start_odometer=11000, + start_fuel_level=37.2, + end_fuel_level=30, + purpose='business', + ) + db.session.add(trip) + db.session.commit() + assert trip.fuel_consumption == pytest.approx(7.2) + + def test_trip_fuel_consumption_human_readable_property(self, app, test_user, sample_vehicle): + trip = Trip( + vehicle_id=sample_vehicle.id, + user_id=test_user.id, + date=date(2024, 1, 10), + start_odometer=11000, + start_fuel_level=37.2, + end_fuel_level=30, + purpose='business', + ) + db.session.add(trip) + db.session.commit() + assert trip.fuel_consumption_human_readable == '~3.6 L' + def test_trip_to_dict(self, app, test_user, sample_vehicle): trip = Trip( vehicle_id=sample_vehicle.id, @@ -676,6 +704,8 @@ def test_trip_to_dict(self, app, test_user, sample_vehicle): date=date(2024, 1, 10), start_odometer=10000.0, end_odometer=10150.0, + start_fuel_level=59.3, + end_fuel_level=56.8, purpose='personal', start_location='Home', end_location='Office', diff --git a/tests/test_trips.py b/tests/test_trips.py index e787342..902a920 100644 --- a/tests/test_trips.py +++ b/tests/test_trips.py @@ -13,6 +13,8 @@ def sample_trip(app, test_user, sample_vehicle): date=date(2024, 2, 1), start_odometer=10000.0, end_odometer=10150.0, + start_fuel_level=100, + end_fuel_level=87.3, purpose='business', description='Client meeting', ) @@ -53,6 +55,8 @@ def test_create_trip(self, auth_client, sample_vehicle, test_user): 'date': '2024-03-01', 'start_odometer': '12000', 'end_odometer': '12200', + 'start_fuel_level': '85.3', + 'end_fuel_level': '80', 'purpose': 'business', 'description': 'Business trip', 'start_location': 'Office', @@ -63,6 +67,8 @@ def test_create_trip(self, auth_client, sample_vehicle, test_user): assert trip is not None assert trip.start_odometer == 12000.0 assert trip.end_odometer == 12200.0 + assert trip.start_fuel_level == 85.3 + assert trip.send_fuel_level == 80.0 assert trip.user_id == test_user.id class TestTripNewNoEndOdometer: @@ -89,7 +95,9 @@ def test_create_trip(self, auth_client, sample_vehicle, test_user): trip = Trip.query.filter_by(description='No end odometer trip').first() assert trip is not None assert trip.start_odometer == 12000.0 - assert trip.end_odometer == None + assert trip.end_odometer is None + assert trip.start_fuel_level is None + assert trip.end_fuel_level is None assert trip.user_id == test_user.id From 9411f062dbdd83d0bb5d18c91ad249aa765381fd Mon Sep 17 00:00:00 2001 From: chaptergy Date: Sun, 16 Aug 2026 21:00:20 +0200 Subject: [PATCH 2/3] Code review changes --- app/models.py | 26 ++++++++++++------- app/routes/api.py | 12 ++++++--- app/routes/trips.py | 26 +++++++++++++++++-- app/templates/trips/form.html | 4 +-- app/templates/trips/index.html | 4 +-- app/templates/vehicles/view.html | 2 +- .../cc6e159f098a_adds_fuel_level_to_trip.py | 13 +++------- tests/test_models.py | 2 ++ tests/test_trips.py | 2 +- 9 files changed, 60 insertions(+), 31 deletions(-) diff --git a/app/models.py b/app/models.py index 0762428..b860601 100644 --- a/app/models.py +++ b/app/models.py @@ -435,13 +435,13 @@ def get_last_odometer(self, distance_unit=None): return max(fuel_odo, trip_odo, charge_odo) - def get_last_fuel_level(self, distance_unit=None): + def get_last_fuel_level(self) -> float | None: """Get the most recent fuel level. - Returns the highest from trips or charging sessions. + Returns the latest from trips or charging sessions. """ # If Tessie is enabled, use Tessie battery level exclusively - if self.uses_tessie_battery() and self.tessie_battery_level: + if self.uses_tessie_battery() and self.tessie_battery_level is not None: return round(self.tessie_battery_level) last_trip = self.trips.filter(Trip.end_fuel_level.isnot(None)).order_by(Trip.date.desc()).first() @@ -449,9 +449,12 @@ def get_last_fuel_level(self, distance_unit=None): last_charge = self.charging_sessions.filter(ChargingSession.end_soc.isnot(None)).order_by( ChargingSession.date.desc()).first() - if not last_trip and not last_charge: return 0 - if not last_trip: return last_charge.end_soc - if not last_charge: return last_trip.end_fuel_level + if not last_trip and not last_charge: + return None + if not last_trip: + return last_charge.end_soc + if not last_charge: + return last_trip.end_fuel_level return last_trip.end_fuel_level if last_trip.date > last_charge.date else last_charge.end_soc @@ -1403,13 +1406,16 @@ def fuel_consumption(self) -> float | None: def fuel_consumption_human_readable(self) -> str | None: """Calculate trip fuel consumption with a plus sign instead of negative for negative fuel consumption""" consumption = self.fuel_consumption - if consumption is None: return None + if consumption is None: + return None if self.vehicle.tank_capacity: abs_consumption = self.vehicle.tank_capacity * consumption / 100 - if abs_consumption < 0: return "~+{:.1f} L".format(abs(abs_consumption)) + if abs_consumption < 0: + return "~+{:.1f} L".format(abs(abs_consumption)) return "~{:.1f} L".format(abs_consumption) - if consumption < 0: return '+' + str(abs(consumption)) + ' %' - return str(consumption) + ' %' + if consumption < 0: + return '+' + "~{:.1f} L".format(abs(consumption)) + ' %' + return "~{:.1f} L".format(consumption) + ' %' def to_dict(self): """Serialize trip to dictionary for API""" diff --git a/app/routes/api.py b/app/routes/api.py index 1886dc6..94b6207 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -2743,7 +2743,7 @@ def get_import_fields(data_type): 'cost': ['cost', 'amount', 'total', 'price', 'expense'], 'vendor': ['vendor', 'shop', 'store', 'supplier', 'merchant', 'provider'], 'start_odometer': ['start odometer', 'start odo', 'start miles', 'start km', 'odometer start'], - 'end_fuel_level': ['end fuel level', 'end fuel', 'end gas', 'fuel level', 'fuel', 'end battery', 'battery'], + 'end_fuel_level': ['end fuel level', 'end fuel', 'end gas', 'end battery'], 'start_fuel_level': ['start fuel level', 'start fuel', 'start gas', 'start battery'], 'end_odometer': ['end odometer', 'end odo', 'end miles', 'end km', 'odometer end'], 'purpose': ['purpose', 'trip purpose', 'reason', 'trip type'], @@ -2957,6 +2957,12 @@ def create_record(data_type, mapped_row, vehicle_id, user_id, date_format, user_ raise ValueError('Missing or invalid start odometer') if end_odo is None: raise ValueError('Missing or invalid end odometer') + start_fuel_level = parse_float_value(mapped_row.get('start_fuel_level')) + end_fuel_level = parse_float_value(mapped_row.get('end_fuel_level')) + if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): + raise ValueError('Start fuel level is outside of allowed 0 - 100 range') + if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): + raise ValueError('Start fuel level is outside of allowed 0 - 100 range') purpose = mapped_row.get('purpose', '').strip().lower() valid_purposes = [p[0] for p in TRIP_PURPOSES] if purpose not in valid_purposes: @@ -2967,8 +2973,8 @@ def create_record(data_type, mapped_row, vehicle_id, user_id, date_format, user_ date=date_val, start_odometer=start_odo, end_odometer=end_odo, - start_fuel_level=parse_float_value(mapped_row.get('start_fuel_level')), - end_fuel_level=parse_float_value(mapped_row.get('end_fuel_level')), + start_fuel_level=start_fuel_level, + end_fuel_level=end_fuel_level, purpose=purpose, description=mapped_row.get('description', '').strip() or None, start_location=mapped_row.get('start_location', '').strip() or None, diff --git a/app/routes/trips.py b/app/routes/trips.py index 01a3329..d521c1c 100644 --- a/app/routes/trips.py +++ b/app/routes/trips.py @@ -78,12 +78,24 @@ def new(): date_str = request.form.get('date') date = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else datetime.now().date() + start_fuel_level = parse_decimal(request.form.get('start_fuel_level')) if request.form.get( + 'start_fuel_level') else None + end_fuel_level = parse_decimal(request.form.get('end_fuel_level')) if request.form.get( + 'end_fuel_level') else None + + if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): + flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') # + if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): + flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') + trip = Trip( vehicle_id=vehicle_id, user_id=current_user.id, date=date, start_odometer=parse_decimal(request.form.get('start_odometer')), end_odometer=parse_decimal(request.form.get('end_odometer')) if request.form.get('end_odometer') else None, + start_fuel_level=start_fuel_level, + end_fuel_level=end_fuel_level, purpose=request.form.get('purpose'), description=request.form.get('description'), start_location=request.form.get('start_location'), @@ -140,12 +152,22 @@ def edit(trip_id): return redirect(url_for('trips.index')) if request.method == 'POST': + start_fuel_level = parse_decimal(request.form.get('start_fuel_level')) if request.form.get( + 'start_fuel_level') else None + end_fuel_level = parse_decimal(request.form.get('end_fuel_level')) if request.form.get( + 'end_fuel_level') else None + + if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): + flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error')# + if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): + flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') + date_str = request.form.get('date') trip.date = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else trip.date trip.start_odometer = parse_decimal(request.form.get('start_odometer')) trip.end_odometer = parse_decimal(request.form.get('end_odometer')) if request.form.get('end_odometer') else None - trip.start_fuel_level = parse_decimal(request.form.get('start_fuel_level')) if request.form.get('start_fuel_level') else None - trip.end_fuel_level = parse_decimal(request.form.get('end_fuel_level')) if request.form.get('end_fuel_level') else None + trip.start_fuel_level = start_fuel_level + trip.end_fuel_level = end_fuel_level trip.purpose = request.form.get('purpose') trip.description = request.form.get('description') trip.start_location = request.form.get('start_location') diff --git a/app/templates/trips/form.html b/app/templates/trips/form.html index adf688d..53daf4a 100644 --- a/app/templates/trips/form.html +++ b/app/templates/trips/form.html @@ -90,7 +90,7 @@

{% if trip %}{
- @@ -208,7 +208,7 @@

{% if trip %}{ const end = parseFloat(document.getElementById('end_fuel_level').value); const consumption = start - end; let textContent = '0'; - if (start && end) { + if (Number.isFinite(start) && Number.isFinite(end)) { if (consumption > 0) textContent = consumption.toFixed(1); else if (consumption < 0) textContent = '+' + Math.abs(consumption).toFixed(1); } diff --git a/app/templates/trips/index.html b/app/templates/trips/index.html index 291cee9..edc51f9 100644 --- a/app/templates/trips/index.html +++ b/app/templates/trips/index.html @@ -119,11 +119,11 @@

{{ _('Trips') }}

{{ "%.1f"|format(trip.distance) }} {{ current_user.distance_unit }} - {% if trip.fuel_consumption %}· {{ trip.fuel_consumption_human_readable }}{% endif %} + {% if trip.fuel_consumption_human_readable is not none %}· {{ trip.fuel_consumption_human_readable }}{% endif %}

{{ "%.0f"|format(trip.start_odometer) }} → {{ "%.0f"|format(trip.end_odometer or 0) }} - {% if trip.fuel_consumption %}· {{ trip.start_fuel_level or "?" }} % → {{ trip.end_fuel_level or "?" }} %{% endif %} + {% if trip.start_fuel_level is not none and trip.end_fuel_level is not none %}· {{ trip.start_fuel_level }} % → {{ trip.end_fuel_level }} %{% endif %}

diff --git a/app/templates/vehicles/view.html b/app/templates/vehicles/view.html index cc50537..3b27c9a 100644 --- a/app/templates/vehicles/view.html +++ b/app/templates/vehicles/view.html @@ -65,7 +65,7 @@

{{ vehicle.name }}<

{{ _('Last Fuel Level') }}
-
{{ "%.0f"|format(vehicle.get_last_fuel_level())}} %
+
{% if vehicle.get_last_fuel_level() is not none %}{{ "%.0f"|format(vehicle.get_last_fuel_level())}} %{% else %}–{% endif %}
diff --git a/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py index 3966ed4..3662593 100644 --- a/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py +++ b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py @@ -17,16 +17,9 @@ def upgrade(): - bind = op.get_bind() - inspector = sa.inspect(bind) - if 'trips' in inspector.get_table_names(): - existing_cols_trip = [col['name'] for col in inspector.get_columns('trips')] - if 'start_fuel_level' not in existing_cols_trip or 'end_fuel_level' not in existing_cols_trip: - with op.batch_alter_table('trips', schema=None) as batch_op: - if 'start_fuel_level' not in existing_cols_trip: - batch_op.add_column(sa.Column('start_fuel_level', sa.Float(), nullable=True)) - if 'end_fuel_level' not in existing_cols_trip: - batch_op.add_column(sa.Column('end_fuel_level', sa.Float(), nullable=True)) + with op.batch_alter_table('trips', schema=None) as batch_op: + batch_op.add_column(sa.Column('start_fuel_level', sa.Float(), nullable=True)) + batch_op.add_column(sa.Column('end_fuel_level', sa.Float(), nullable=True)) def downgrade(): diff --git a/tests/test_models.py b/tests/test_models.py index 0400ac8..471ffda 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -718,6 +718,8 @@ def test_trip_to_dict(self, app, test_user, sample_vehicle): assert d['purpose'] == 'personal' assert d['start_location'] == 'Home' assert d['end_location'] == 'Office' + assert d['start_fuel_level'] == 59.3 + assert d['end_fuel_level'] == 56.8 assert 'id' in d assert 'vehicle_id' in d diff --git a/tests/test_trips.py b/tests/test_trips.py index 902a920..592a8a0 100644 --- a/tests/test_trips.py +++ b/tests/test_trips.py @@ -68,7 +68,7 @@ def test_create_trip(self, auth_client, sample_vehicle, test_user): assert trip.start_odometer == 12000.0 assert trip.end_odometer == 12200.0 assert trip.start_fuel_level == 85.3 - assert trip.send_fuel_level == 80.0 + assert trip.end_fuel_level == 80.0 assert trip.user_id == test_user.id class TestTripNewNoEndOdometer: From 824c963ab8054a6862a73d5d0a9d4efffdfec016 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Sun, 16 Aug 2026 21:13:32 +0200 Subject: [PATCH 3/3] More code review changes --- app/routes/api.py | 2 +- app/routes/trips.py | 9 +++++---- .../versions/cc6e159f098a_adds_fuel_level_to_trip.py | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/routes/api.py b/app/routes/api.py index 94b6207..9ed810c 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -2962,7 +2962,7 @@ def create_record(data_type, mapped_row, vehicle_id, user_id, date_format, user_ if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): raise ValueError('Start fuel level is outside of allowed 0 - 100 range') if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): - raise ValueError('Start fuel level is outside of allowed 0 - 100 range') + raise ValueError('End fuel level is outside of allowed 0 - 100 range') purpose = mapped_row.get('purpose', '').strip().lower() valid_purposes = [p[0] for p in TRIP_PURPOSES] if purpose not in valid_purposes: diff --git a/app/routes/trips.py b/app/routes/trips.py index d521c1c..ef7ddb7 100644 --- a/app/routes/trips.py +++ b/app/routes/trips.py @@ -6,6 +6,7 @@ from app.utils import parse_decimal from flask import jsonify from app.models import Vehicle, Trip, TripTemplate, TRIP_PURPOSES +import math bp = Blueprint('trips', __name__, url_prefix='/trips') @@ -83,10 +84,10 @@ def new(): end_fuel_level = parse_decimal(request.form.get('end_fuel_level')) if request.form.get( 'end_fuel_level') else None - if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): + if start_fuel_level is not None and (not math.isfinite(start_fuel_level) or start_fuel_level < 0. or start_fuel_level > 100.): flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') # - if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): - flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') + if end_fuel_level is not None and (not math.isfinite(end_fuel_level) or end_fuel_level < 0. or end_fuel_level > 100.): + flash(_('End fuel level is outside of allowed 0 - 100 range'), 'error') trip = Trip( vehicle_id=vehicle_id, @@ -160,7 +161,7 @@ def edit(trip_id): if start_fuel_level is not None and (start_fuel_level < 0. or start_fuel_level > 100.): flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error')# if end_fuel_level is not None and (end_fuel_level < 0. or end_fuel_level > 100.): - flash(_('Start fuel level is outside of allowed 0 - 100 range'), 'error') + flash(_('End fuel level is outside of allowed 0 - 100 range'), 'error') date_str = request.form.get('date') trip.date = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else trip.date diff --git a/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py index 3662593..ac6abf1 100644 --- a/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py +++ b/migrations/versions/cc6e159f098a_adds_fuel_level_to_trip.py @@ -17,12 +17,14 @@ def upgrade(): + """ Adds start and end fuel level fields to trips table """ with op.batch_alter_table('trips', schema=None) as batch_op: batch_op.add_column(sa.Column('start_fuel_level', sa.Float(), nullable=True)) batch_op.add_column(sa.Column('end_fuel_level', sa.Float(), nullable=True)) def downgrade(): + """ Removes start and end fuel level fields from the trips table """ with op.batch_alter_table('trips', schema=None) as batch_op: batch_op.drop_column('start_fuel_level') batch_op.drop_column('end_fuel_level')