{{ nav_tab(vehicle.id|string + '_odometer', 'Odometer', true) }}
+ {{ nav_tab(vehicle.id|string + '_costs', 'Costs', false) }}
{% for consumable in vehicle.consumables %}
{{ nav_tab(vehicle.id|string + '_' + consumable.id|string, consumable.name, false) }}
{% endfor %}
@@ -131,6 +147,17 @@
true
)
}}
+ {{ tab_pane(
+ vehicle.id|string + '_costs',
+ chart(
+ vehicle.costs,
+ 'ref_' + vehicle.id|string + '_costs',
+ '€',
+ url_for('select_consumable_for_new_pitstop', vid=vehicle.id)
+ ),
+ false
+ )
+ }}
{% for consumable in vehicle.consumables %}
{{ print_consumable_table(consumable) }}
diff --git a/app/tools.py b/app/tools.py
index 9518b77..c7da89f 100644
--- a/app/tools.py
+++ b/app/tools.py
@@ -7,6 +7,15 @@ from .entities import Pitstop, FillingStation
from . import db, app
+class RegularCostInstance:
+ def __init__(self, regular_id, date, odometer, costs, name):
+ self.id = regular_id
+ self.date = date
+ self.odometer = odometer
+ self.costs = costs
+ self.name = name
+
+
class ConsumableStats:
def __init__(self, vehicle, consumable):
self.name = consumable.name
@@ -19,7 +28,9 @@ class ConsumableStats:
self.average_amount = []
self.amounts = []
- pitstops = [stop for stop in vehicle.pitstops if stop.consumable_id == consumable.id]
+ pitstops = [
+ stop for stop in vehicle.pitstops if stop.consumable_id == consumable.id
+ ]
pitstop_count = len(pitstops)
if pitstop_count > 0:
@@ -28,16 +39,27 @@ class ConsumableStats:
self.amounts.append(StatsEvent(pitstop.date, pitstop.amount))
self.average_amount_fuelled = self.overall_amount / pitstop_count
if pitstop_count > 1:
- overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
+ overall_distance = (
+ vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
+ )
self.average_distance = overall_distance / (pitstop_count - 1)
- self.average_amount_used = 100 * (self.overall_amount - pitstops[0].amount) / overall_distance
+ self.average_amount_used = (
+ 100 * (self.overall_amount - pitstops[0].amount) / overall_distance
+ )
for index in range(1, pitstop_count):
last_ps = pitstops[index - 1]
current_ps = pitstops[index]
self.average_amount.append(
StatsEvent(
current_ps.date,
- round(100 * current_ps.amount / (current_ps.odometer - last_ps.odometer), 2)))
+ round(
+ 100
+ * current_ps.amount
+ / (current_ps.odometer - last_ps.odometer),
+ 2,
+ ),
+ )
+ )
class VehicleStats:
@@ -48,6 +70,8 @@ class VehicleStats:
self.overall_costs = 0
self.consumables = []
self.odometers = []
+ self.costs = []
+ self.costs_per_distance = "N/A"
for consumable in vehicle.consumables:
self.consumables.append(ConsumableStats(vehicle, consumable))
@@ -60,27 +84,47 @@ class VehicleStats:
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
if pitstop.costs is not None:
self.overall_costs += pitstop.costs
+ self.costs.append(StatsEvent(pitstop.date, pitstop.costs))
+
+ # add the instances to the overall costs
+ for regular_cost_instance in calculate_regular_cost_instances(vehicle):
+ self.overall_costs += regular_cost_instance.costs
+ self.costs.append(
+ StatsEvent(regular_cost_instance.date, regular_cost_instance.costs)
+ )
if pitstop_count > 1:
self.overall_distance = events[-1].odometer - events[0].odometer
+ self.costs.sort(key=lambda x: x.date)
+ accumulated_costs = 0
+ for c in self.costs:
+ accumulated_costs += c.value
+ c.value = accumulated_costs
+
+ if self.overall_distance > 0:
+ self.costs_per_distance = float(self.overall_costs) / (float(self.overall_distance) / 100)
+
class StatsEvent:
def __init__(self, date, value):
self.date = date
self.value = value
+ def __repr__(self):
+ return str(self.date)
+
def db_log_add(entity):
- logging.info('db_add: %s' % str(entity))
+ logging.info("db_add: %s" % str(entity))
def db_log_delete(entity):
- logging.info('db_delete: %s' % str(entity))
+ logging.info("db_delete: %s" % str(entity))
def db_log_update(entity):
- logging.info('db_update: %s' % str(entity))
+ logging.info("db_update: %s" % str(entity))
def check_vehicle_name_is_unique(current_user, name_field):
@@ -107,10 +151,11 @@ def get_latest_pitstop_for_vehicle(vehicle_id):
:param vehicle_id: the id of the vehicle
:return: the latest pitstop or None if no pitstop exists
"""
- latest_pitstop = Pitstop.query \
- .filter(Pitstop.vehicle_id == vehicle_id) \
- .order_by(Pitstop.id.desc()) \
+ latest_pitstop = (
+ Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
+ .order_by(Pitstop.id.desc())
.first()
+ )
return latest_pitstop
@@ -121,15 +166,18 @@ def get_latest_pitstop_for_vehicle_and_consumable(vehicle_id, consumable_id):
:param consumable_id: the id of the consumable
:return: the latest pitstop or None if no pitstop exists
"""
- latest_pitstop_consumable = Pitstop.query \
- .filter(Pitstop.vehicle_id == vehicle_id) \
- .filter(Pitstop.consumable_id == consumable_id) \
- .order_by(Pitstop.id.desc()) \
+ latest_pitstop_consumable = (
+ Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
+ .filter(Pitstop.consumable_id == consumable_id)
+ .order_by(Pitstop.id.desc())
.first()
+ )
return latest_pitstop_consumable
-def compute_lower_limits_for_new_pitstop(latest_pitstop, last_pitstop_consumable, consumable_id):
+def compute_lower_limits_for_new_pitstop(
+ latest_pitstop, last_pitstop_consumable, consumable_id
+):
"""
This function figures out the lower limits for date and odometer of a new pitstop.
:param latest_pitstop:
@@ -154,7 +202,7 @@ def compute_lower_limits_for_new_pitstop(latest_pitstop, last_pitstop_consumable
def pitstop_service_key(x):
- return x.odometer, x.date
+ return x.date, x.odometer
def get_event_line_for_vehicle(vehicle):
@@ -170,45 +218,79 @@ def get_event_line_for_vehicle(vehicle):
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
- yield l[i:i + n]
+ yield l[i : i + n]
def update_filling_station_prices(ids):
- max_age = (datetime.now() - timedelta(minutes=15)).strftime('%Y-%m-%d %H:%M')
+ max_age = (datetime.now() - timedelta(minutes=15)).strftime("%Y-%m-%d %H:%M")
- res = db.session. \
- query(FillingStation). \
- filter(FillingStation.id.in_(ids)). \
- filter(or_(FillingStation.last_update == None, FillingStation.last_update < max_age)). \
- all()
+ res = (
+ db.session.query(FillingStation)
+ .filter(FillingStation.id.in_(ids))
+ .filter(
+ or_(
+ FillingStation.last_update == None, FillingStation.last_update < max_age
+ )
+ )
+ .all()
+ )
if len(res) > 0:
id_map = {x.id: x for x in res}
query_ids = [x.id for x in res]
- api_key = app.config['TANKERKOENIG_API_KEY']
- url = 'https://creativecommons.tankerkoenig.de/json/prices.php'
+ api_key = app.config["TANKERKOENIG_API_KEY"]
+ url = "https://creativecommons.tankerkoenig.de/json/prices.php"
# documentation tells us to query max 10 filling stations at a time...
for c in chunks(query_ids, 10):
- params = {
- 'apikey': api_key, 'ids': ','.join(c)
- }
+ params = {"apikey": api_key, "ids": ",".join(c)}
response = requests.get(url, params=params)
response_json = response.json()
- if response_json['ok']:
- print(response_json)
- prices = response_json['prices']
+ if response_json["ok"]:
+ prices = response_json["prices"]
for price in prices:
id = price
station_status = prices[id]
- id_map[id].open = station_status['status'] == 'open'
+ id_map[id].open = station_status["status"] == "open"
if id_map[id].open:
- id_map[id].diesel = station_status['diesel']
- id_map[id].e10 = station_status['e10']
- id_map[id].e5 = station_status['e5']
+ id_map[id].diesel = station_status["diesel"]
+ id_map[id].e10 = station_status["e10"]
+ id_map[id].e5 = station_status["e5"]
id_map[id].last_update = datetime.now()
else:
logging.error(
- 'could not update filling stations because of {r} on URL {u}.'.format(r=str(response_json),
- u=response.url))
+ "could not update filling stations because of {r} on URL {u}.".format(
+ r=str(response_json), u=response.url
+ )
+ )
db.session.commit()
+
+
+def calculate_regular_cost_instances(vehicle):
+ data = []
+ for regular in vehicle.regulars:
+ if date.today() < regular.start_at:
+ # skip regular costs that are not yet active
+ continue
+ if regular.ends_at:
+ end_date = regular.ends_at
+ else:
+ end_date = date.today()
+
+ start_year = regular.start_at.year
+ end_year = end_date.year
+
+ for year in range(start_year, end_year + 1):
+ for day in regular.days.split(","):
+ m, d = day.split("-")
+ d = date(year, int(m), int(d))
+ if regular.start_at <= d and d <= end_date:
+ r = RegularCostInstance(
+ regular.id,
+ date=d,
+ odometer=None,
+ costs=regular.costs,
+ name=regular.description,
+ )
+ data.append(r)
+ return data
diff --git a/config.py b/config.py
index a2d6bb9..9166ff5 100644
--- a/config.py
+++ b/config.py
@@ -30,7 +30,7 @@ class Config:
class DevelopmentConfig(Config):
SECURITY_SEND_REGISTER_EMAIL = False
DEBUG = True
- SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
+ SQLALCHEMY_DATABASE_URI = 'sqlite:////data/data.sqlite'
class TestingConfig(Config):
diff --git a/requirements.txt b/requirements.txt
index 321d734..0e5305f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,4 +5,5 @@ Flask-WTF
PyMySQL
markdown
Flask-Limiter
-requests
\ No newline at end of file
+requests
+email_validator