From 981b940e5cd3296560afefc4a1bb3fcf92f8fbab Mon Sep 17 00:00:00 2001 From: GoNzCiD Date: Fri, 14 Aug 2026 13:50:21 +0200 Subject: [PATCH 1/6] Add support for MySQL/MariaDB --- README.md | 3 ++- requirements.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fe7755..2187578 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,9 @@ SECRET_KEY=your-secure-random-string # Database location (default: SQLite) DATABASE_URL=sqlite:///data/may.db -# PostgreSQL is also supported: +# PostgreSQL, MySQL, and MariaDB are also supported: # DATABASE_URL=postgresql://user:password@host:5432/may +# DATABASE_URL=mysql+pymysql://user:password@host:3306/may # Upload folder for attachments UPLOAD_FOLDER=/app/data/uploads diff --git a/requirements.txt b/requirements.txt index e6cd229..18e5958 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ weasyprint>=69.0 requests>=2.34.2 python-dateutil>=2.9.0.post0 psycopg2-binary>=2.9.10 +pymysql>=1.2.0 pytest>=9.1.1 pytest-cov>=7.1.0 coverage>=7.15.1 From f4df45eff078e7cfd38d56b70411d0421647e5c0 Mon Sep 17 00:00:00 2001 From: GoNzCiD Date: Fri, 14 Aug 2026 15:47:38 +0200 Subject: [PATCH 2/6] Automatically calculate the cost per unit --- app/routes/fuel.py | 6 ++++++ app/templates/fuel/form.html | 23 +++++++++++++++++------ tests/test_fuel.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/app/routes/fuel.py b/app/routes/fuel.py index 71a5654..4c8d2ef 100644 --- a/app/routes/fuel.py +++ b/app/routes/fuel.py @@ -93,6 +93,12 @@ def new(): flash(err, 'error') return render_template('fuel/new.html', vehicles=vehicles) + # Derive the unit price from the amount paid when it was omitted. + # Add the discount back because total_cost represents the amount paid + # after the per-unit discount has been applied (#209). + if price_per_unit is None and volume and total_cost is not None: + price_per_unit = round(total_cost / volume + (discount_per_unit or 0), 3) + log = FuelLog( vehicle_id=vehicle_id, user_id=current_user.id, diff --git a/app/templates/fuel/form.html b/app/templates/fuel/form.html index dd7fb33..27eb67d 100644 --- a/app/templates/fuel/form.html +++ b/app/templates/fuel/form.html @@ -62,7 +62,7 @@

{% if log %}{{ @@ -70,7 +70,7 @@

{% if log %}{{ @@ -78,7 +78,7 @@

{% if log %}{{

{{ _('Optional loyalty discount, subtracted from the price per unit.') }}

@@ -88,6 +88,7 @@

{% if log %}{{ @@ -243,13 +244,23 @@

{% if log %}{{ } } -function calculateTotal() { +function calculateFuelAmounts() { const volume = parseDecimal(document.getElementById('volume').value) || 0; - const pricePerUnit = parseDecimal(document.getElementById('price_per_unit').value) || 0; + const priceInput = document.getElementById('price_per_unit'); + const totalInput = document.getElementById('total_cost'); + let pricePerUnit = parseDecimal(priceInput.value) || 0; const discountPerUnit = parseDecimal(document.getElementById('discount_per_unit').value) || 0; + + // When the price is omitted, derive it from the total paid and volume. + // Keep a manually entered price untouched. + if (!priceInput.value && volume && totalInput.value) { + pricePerUnit = (parseDecimal(totalInput.value) / volume) + discountPerUnit; + priceInput.value = pricePerUnit.toFixed(3); + } + if (volume && pricePerUnit) { const effectivePrice = Math.max(pricePerUnit - discountPerUnit, 0); - document.getElementById('total_cost').value = (volume * effectivePrice).toFixed(2); + totalInput.value = (volume * effectivePrice).toFixed(2); } } diff --git a/tests/test_fuel.py b/tests/test_fuel.py index 58007c1..6ee4b83 100644 --- a/tests/test_fuel.py +++ b/tests/test_fuel.py @@ -98,6 +98,34 @@ def test_no_discount_is_none(self, auth_client, sample_vehicle): assert log.discount_per_unit is None assert log.total_cost == 60.0 + def test_price_per_unit_is_calculated_from_total_cost(self, auth_client, sample_vehicle): + resp = auth_client.post('/fuel/new', data={ + 'vehicle_id': str(sample_vehicle.id), + 'date': '2024-03-05', + 'odometer': '15400', + 'volume': '40.0', + 'total_cost': '64.0', + 'is_full_tank': 'on', + }, follow_redirects=True) + assert resp.status_code == 200 + log = FuelLog.query.filter_by(vehicle_id=sample_vehicle.id, odometer=15400.0).first() + assert log is not None + assert log.price_per_unit == 1.6 + + def test_calculated_price_includes_discount(self, auth_client, sample_vehicle): + auth_client.post('/fuel/new', data={ + 'vehicle_id': str(sample_vehicle.id), + 'date': '2024-03-06', + 'odometer': '15500', + 'volume': '40.0', + 'discount_per_unit': '0.10', + 'total_cost': '60.0', + 'is_full_tank': 'on', + }, follow_redirects=True) + log = FuelLog.query.filter_by(vehicle_id=sample_vehicle.id, odometer=15500.0).first() + assert log is not None + assert log.price_per_unit == 1.6 + def test_new_redirects_to_vehicles_if_none(self, auth_client): # No vehicles exist for this user resp = auth_client.get('/fuel/new', follow_redirects=False) From c6a096a08e0a202598ea22a6a7112e1770488faf Mon Sep 17 00:00:00 2001 From: GoNzCiD Date: Mon, 17 Aug 2026 02:12:13 +0200 Subject: [PATCH 3/6] add checks & tests --- app/routes/fuel.py | 6 ++++++ app/security.py | 5 ++++- tests/test_fuel.py | 17 +++++++++++++++++ tests/test_security.py | 5 +++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/routes/fuel.py b/app/routes/fuel.py index 4c8d2ef..5df8b40 100644 --- a/app/routes/fuel.py +++ b/app/routes/fuel.py @@ -98,6 +98,12 @@ def new(): # after the per-unit discount has been applied (#209). if price_per_unit is None and volume and total_cost is not None: price_per_unit = round(total_cost / volume + (discount_per_unit or 0), 3) + price_per_unit, err = validate_positive_number( + price_per_unit, 'Price per unit', max_value=1000 + ) + if err: + flash(err, 'error') + return redirect(url_for('fuel.new')) log = FuelLog( vehicle_id=vehicle_id, diff --git a/app/security.py b/app/security.py index 3d68f68..26e4fc0 100644 --- a/app/security.py +++ b/app/security.py @@ -7,6 +7,7 @@ from functools import wraps from flask import request, redirect, url_for, flash from flask_login import current_user +from app.utils import parse_decimal # File signature (magic bytes) mappings FILE_SIGNATURES = { @@ -180,9 +181,11 @@ def validate_positive_number(value, field_name, max_value=None, allow_zero=True) return None, None # Empty is OK try: - num = float(value) + num = parse_decimal(value) except (ValueError, TypeError): return None, f"{field_name} must be a valid number" + if num is None: + return None, None if not allow_zero and num == 0: return None, f"{field_name} cannot be zero" diff --git a/tests/test_fuel.py b/tests/test_fuel.py index 6ee4b83..758be8b 100644 --- a/tests/test_fuel.py +++ b/tests/test_fuel.py @@ -112,6 +112,23 @@ def test_price_per_unit_is_calculated_from_total_cost(self, auth_client, sample_ assert log is not None assert log.price_per_unit == 1.6 + def test_calculated_price_respects_maximum(self, auth_client, sample_vehicle, sample_station): + resp = auth_client.post('/fuel/new', data={ + 'vehicle_id': str(sample_vehicle.id), + 'date': '2024-03-05', + 'odometer': '15401', + 'volume': '1', + 'total_cost': '2000', + 'station_id': str(sample_station.id), + }, follow_redirects=True) + assert resp.status_code == 200 + assert FuelLog.query.filter_by( + vehicle_id=sample_vehicle.id, odometer=15401.0 + ).first() is None + assert FuelPriceHistory.query.filter_by( + station_id=sample_station.id + ).first() is None + def test_calculated_price_includes_discount(self, auth_client, sample_vehicle): auth_client.post('/fuel/new', data={ 'vehicle_id': str(sample_vehicle.id), diff --git a/tests/test_security.py b/tests/test_security.py index 3b54584..1423025 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -155,6 +155,11 @@ def test_string_number(self): assert abs(val - 10.5) < 0.001 assert error is None + def test_comma_decimal(self): + val, error = validate_positive_number('9,99', 'price') + assert val == 9.99 + assert error is None + def test_invalid_string(self): val, error = validate_positive_number('abc', 'cost') assert val is None From 76328fa6d21b74314e19211763cf0b2c69e3e885 Mon Sep 17 00:00:00 2001 From: GoNzCiD Date: Mon, 17 Aug 2026 14:32:34 +0200 Subject: [PATCH 4/6] fix: preserve explicit total when deriving fuel unit price --- app/templates/fuel/form.html | 36 ++++++++++++++++++++++++++++++------ app/utils.py | 7 ++++++- tests/test_security.py | 10 ++++++++++ tests/test_utils.py | 4 ++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/templates/fuel/form.html b/app/templates/fuel/form.html index 27eb67d..db2a8e2 100644 --- a/app/templates/fuel/form.html +++ b/app/templates/fuel/form.html @@ -180,6 +180,8 @@

{% if log %}{{