Merge branch 'issue_4_add_cost_field' into 'development'

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

See merge request !14
This commit is contained in:
Joachim Lusiardi 2016-06-22 21:01:46 +02:00
commit 6d26ceeae8
8 changed files with 96 additions and 5 deletions

View File

@ -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`

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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: