adds tracking of costs
This commit adds a new field to each pitstop for tracking of costs. This includes: * a new DB column * a new field in the create pitstop form * a new column in the pitstop overview * new stats values related to costs * 2 new graphs
This commit is contained in:
parent
2176579baa
commit
5b69a82e05
|
@ -14,6 +14,11 @@ operation.
|
|||
## start database
|
||||
`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mysql:latest`
|
||||
|
||||
## Database migrations
|
||||
### From *Cathrine* to *Master*:
|
||||
|
||||
`ALTER TABLE pitstop ADD COLUMN costs DECIMAL(5,2) NOT NULL DEFAULT 0.0 AFTER vehicle_id;`
|
||||
|
||||
## run in development
|
||||
|
||||
Include the development version of the code as volume, so the app gets
|
||||
|
@ -21,7 +26,7 @@ reloaded automatically. The sqlite file will be stored in *tmp* so it
|
|||
can be inspected with tools like *sqlite3*. The switch *DEBUG* enables
|
||||
debugging during development.
|
||||
|
||||
`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch`
|
||||
`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -e config=../config/config.py --link pitstops_db:database -p 5000:5000 rollerverbrauch`
|
||||
|
||||
## run in production
|
||||
`docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch`
|
||||
|
|
|
@ -188,7 +188,7 @@ def create_pit_stop_form(vid):
|
|||
form.set_pitstop(last_pitstop)
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data)
|
||||
new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data, form.costs.data)
|
||||
db.session.add(new_stop)
|
||||
vehicle.pitstops.append(new_stop)
|
||||
db.session.commit()
|
||||
|
@ -198,10 +198,12 @@ def create_pit_stop_form(vid):
|
|||
form.odometer.default = last_pitstop.odometer
|
||||
form.litres.default = last_pitstop.litres
|
||||
form.date.default = date.today()
|
||||
form.costs.default = last_pitstop.costs
|
||||
form.process()
|
||||
messages = {
|
||||
'date': 'Date must be between %s and %s (including).' % (str(last_pitstop.date), str(date.today())),
|
||||
'odometer': 'Odometer must be greater than %s km.' % (str(last_pitstop.odometer))
|
||||
'odometer': 'Odometer must be greater than %s km.' % (str(last_pitstop.odometer)),
|
||||
'costs': 'Costs must be higher than 0.01 €.'
|
||||
}
|
||||
return render_template('newPitStopForm.html', form=form, vehicle=vehicle, messages = messages)
|
||||
|
||||
|
|
|
@ -58,12 +58,14 @@ class Pitstop(db.Model):
|
|||
date = db.Column(db.Date)
|
||||
odometer = db.Column(db.Integer)
|
||||
litres = db.Column(db.Numeric(5, 2))
|
||||
costs = db.Column(db.Numeric(5, 2), default=0)
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'))
|
||||
|
||||
def __init__(self, odometer, litres, date):
|
||||
def __init__(self, odometer, litres, date, costs):
|
||||
self.odometer = odometer
|
||||
self.litres = litres
|
||||
self.date = date
|
||||
self.costs = costs
|
||||
|
||||
def __repr__(self):
|
||||
return '<Pitstop odometer="%r" litres="%r" date="%r" vehicle_id="%r">' % (self.odometer, self.litres, self.date, self.vehicle_id)
|
|
@ -21,6 +21,11 @@ def litres_check(form, field):
|
|||
raise ValidationError('You must fuel at least 0.1 l')
|
||||
|
||||
|
||||
def costs_check(form, field):
|
||||
if field.data is not None and field.data <= 0:
|
||||
raise ValidationError('Costs must be above 0.01 €.')
|
||||
|
||||
|
||||
class SelectVehicleForm(Form):
|
||||
vehicle = SelectField('Vehicle', coerce=int)
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
@ -30,6 +35,7 @@ class CreatePitstopForm(Form):
|
|||
date = DateField('Date of Pitstop', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
submit = SubmitField(label='Do it!')
|
||||
last_pitstop = None
|
||||
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
{{messages['odometer']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.litres) }}
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
Litres<br/>
|
||||
Average
|
||||
</th>
|
||||
<th>
|
||||
Costs<br />
|
||||
Costs per Litre
|
||||
</th>
|
||||
</tr>
|
||||
{% for pitstop in vehicle.pitstops|reverse %}
|
||||
{% if not loop.last %}
|
||||
|
@ -49,6 +53,10 @@
|
|||
{{pitstop.litres}} l<br/>
|
||||
{{average | round(2)}} l/100km
|
||||
</td>
|
||||
<td>
|
||||
{{pitstop.costs}} €<br />
|
||||
{{ (pitstop.costs / pitstop.litres) | round(2) }} €/l
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class='pitstop'>
|
||||
|
@ -64,6 +72,10 @@
|
|||
{{pitstop.litres}} l<br/>
|
||||
-- l/100km
|
||||
</td>
|
||||
<td>
|
||||
{{pitstop.costs}} €<br />
|
||||
{{ (pitstop.costs / pitstop.litres) | round(2) }} €/l
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -41,6 +41,14 @@
|
|||
<th>Average Litres used:</th>
|
||||
<td>{{ vehicle.average_litres_used | round(2) }} l/100km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Logged Costs:</th>
|
||||
<td>{{ vehicle.overall_costs | round(2) }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Average Costs per Litre:</th>
|
||||
<td>{{ vehicle.average_costs_per_litre | round(2) }} €/l</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs">
|
||||
|
@ -59,6 +67,16 @@
|
|||
Odometer
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#v{{vehicle.id}}_c4" id="i{{vehicle.id}}_c4" data-toggle="tab">
|
||||
Costs per litre
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#v{{vehicle.id}}_c5" id="i{{vehicle.id}}_c5" data-toggle="tab">
|
||||
Costs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="charts_tabs-content" class="tab-content">
|
||||
<div class="tab-pane active" id="v{{vehicle.id}}_c3">
|
||||
|
@ -97,6 +115,30 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane " id="v{{vehicle.id}}_c4">
|
||||
{% if vehicle.pitstop_count > 0 %}
|
||||
<div id="costsPerLitre{{vehicle.id}}" style="width:100%; height:500px;"></div>
|
||||
<script type="text/javascript">
|
||||
{{ chartScript('costsPerLitre'+vehicle.id|str, vehicle.costsPerLitre, '€/l') }}
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane " id="v{{vehicle.id}}_c5">
|
||||
{% if vehicle.pitstop_count > 0 %}
|
||||
<div id="costs{{vehicle.id}}" style="width:100%; height:500px;"></div>
|
||||
<script type="text/javascript">
|
||||
{{ chartScript('costs'+vehicle.id|str, vehicle.costs, '€') }}
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -14,13 +14,29 @@ class VehicleStats:
|
|||
self.litres = []
|
||||
self.average_litres = []
|
||||
self.odometers = []
|
||||
self.costsPerLitre = []
|
||||
self.costs = []
|
||||
self.overall_costs = 0
|
||||
self.average_costs_per_litre = 0
|
||||
cost_count = 0;
|
||||
|
||||
if self.pitstop_count > 0:
|
||||
for pitstop in vehicle.pitstops:
|
||||
self.overall_litres += pitstop.litres
|
||||
self.litres.append(StatsEvent(pitstop.date, pitstop.litres))
|
||||
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
|
||||
self.costsPerLitre.append(StatsEvent(pitstop.date, pitstop.costs / pitstop.litres))
|
||||
self.costs.append(StatsEvent(pitstop.date, pitstop.costs))
|
||||
self.overall_costs += pitstop.costs
|
||||
self.average_costs_per_litre += (pitstop.costs / pitstop.litres)
|
||||
if pitstop.costs > 0:
|
||||
cost_count += 1
|
||||
self.average_litres_fuelled = self.overall_litres / self.pitstop_count
|
||||
if cost_count > 0:
|
||||
self.average_costs_per_litre = self.average_costs_per_litre / cost_count
|
||||
else:
|
||||
self.average_costs_per_litre = 0
|
||||
|
||||
if self.pitstop_count > 1:
|
||||
self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
||||
self.average_distance = self.overall_distance / (self.pitstop_count - 1)
|
||||
|
@ -28,7 +44,9 @@ class VehicleStats:
|
|||
for index in range(1, self.pitstop_count):
|
||||
last_ps = vehicle.pitstops[index - 1]
|
||||
current_ps = vehicle.pitstops[index]
|
||||
self.average_litres.append(StatsEvent(current_ps.date, round(100 * current_ps.litres/(current_ps.odometer - last_ps.odometer),2)))
|
||||
self.average_litres.append(StatsEvent(
|
||||
current_ps.date,
|
||||
round(100 * current_ps.litres/(current_ps.odometer - last_ps.odometer), 2)))
|
||||
|
||||
|
||||
class StatsEvent:
|
||||
|
|
Loading…
Reference in New Issue