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:
commit
6d26ceeae8
|
@ -14,6 +14,11 @@ operation.
|
||||||
## start database
|
## start database
|
||||||
`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mysql:latest`
|
`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
|
## run in development
|
||||||
|
|
||||||
Include the development version of the code as volume, so the app gets
|
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
|
can be inspected with tools like *sqlite3*. The switch *DEBUG* enables
|
||||||
debugging during development.
|
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
|
## run in production
|
||||||
`docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch`
|
`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)
|
form.set_pitstop(last_pitstop)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
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)
|
db.session.add(new_stop)
|
||||||
vehicle.pitstops.append(new_stop)
|
vehicle.pitstops.append(new_stop)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -198,10 +198,12 @@ def create_pit_stop_form(vid):
|
||||||
form.odometer.default = last_pitstop.odometer
|
form.odometer.default = last_pitstop.odometer
|
||||||
form.litres.default = last_pitstop.litres
|
form.litres.default = last_pitstop.litres
|
||||||
form.date.default = date.today()
|
form.date.default = date.today()
|
||||||
|
form.costs.default = last_pitstop.costs
|
||||||
form.process()
|
form.process()
|
||||||
messages = {
|
messages = {
|
||||||
'date': 'Date must be between %s and %s (including).' % (str(last_pitstop.date), str(date.today())),
|
'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)
|
return render_template('newPitStopForm.html', form=form, vehicle=vehicle, messages = messages)
|
||||||
|
|
||||||
|
|
|
@ -58,12 +58,14 @@ class Pitstop(db.Model):
|
||||||
date = db.Column(db.Date)
|
date = db.Column(db.Date)
|
||||||
odometer = db.Column(db.Integer)
|
odometer = db.Column(db.Integer)
|
||||||
litres = db.Column(db.Numeric(5, 2))
|
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'))
|
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.odometer = odometer
|
||||||
self.litres = litres
|
self.litres = litres
|
||||||
self.date = date
|
self.date = date
|
||||||
|
self.costs = costs
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Pitstop odometer="%r" litres="%r" date="%r" vehicle_id="%r">' % (self.odometer, self.litres, self.date, self.vehicle_id)
|
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')
|
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):
|
class SelectVehicleForm(Form):
|
||||||
vehicle = SelectField('Vehicle', coerce=int)
|
vehicle = SelectField('Vehicle', coerce=int)
|
||||||
submit = SubmitField(label='Do it!')
|
submit = SubmitField(label='Do it!')
|
||||||
|
@ -30,6 +35,7 @@ class CreatePitstopForm(Form):
|
||||||
date = DateField('Date of Pitstop', validators=[date_check])
|
date = DateField('Date of Pitstop', validators=[date_check])
|
||||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||||
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
|
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
|
||||||
|
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||||
submit = SubmitField(label='Do it!')
|
submit = SubmitField(label='Do it!')
|
||||||
last_pitstop = None
|
last_pitstop = None
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
{{messages['odometer']}}
|
{{messages['odometer']}}
|
||||||
</span>
|
</span>
|
||||||
{{ render_field_with_errors(form.litres) }}
|
{{ 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) }}
|
{{ render_field_with_errors(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,6 +30,10 @@
|
||||||
Litres<br/>
|
Litres<br/>
|
||||||
Average
|
Average
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
Costs<br />
|
||||||
|
Costs per Litre
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for pitstop in vehicle.pitstops|reverse %}
|
{% for pitstop in vehicle.pitstops|reverse %}
|
||||||
{% if not loop.last %}
|
{% if not loop.last %}
|
||||||
|
@ -49,6 +53,10 @@
|
||||||
{{pitstop.litres}} l<br/>
|
{{pitstop.litres}} l<br/>
|
||||||
{{average | round(2)}} l/100km
|
{{average | round(2)}} l/100km
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{pitstop.costs}} €<br />
|
||||||
|
{{ (pitstop.costs / pitstop.litres) | round(2) }} €/l
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr class='pitstop'>
|
<tr class='pitstop'>
|
||||||
|
@ -64,6 +72,10 @@
|
||||||
{{pitstop.litres}} l<br/>
|
{{pitstop.litres}} l<br/>
|
||||||
-- l/100km
|
-- l/100km
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{pitstop.costs}} €<br />
|
||||||
|
{{ (pitstop.costs / pitstop.litres) | round(2) }} €/l
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -41,6 +41,14 @@
|
||||||
<th>Average Litres used:</th>
|
<th>Average Litres used:</th>
|
||||||
<td>{{ vehicle.average_litres_used | round(2) }} l/100km</td>
|
<td>{{ vehicle.average_litres_used | round(2) }} l/100km</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs">
|
<ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs">
|
||||||
|
@ -59,6 +67,16 @@
|
||||||
Odometer
|
Odometer
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
<div id="charts_tabs-content" class="tab-content">
|
<div id="charts_tabs-content" class="tab-content">
|
||||||
<div class="tab-pane active" id="v{{vehicle.id}}_c3">
|
<div class="tab-pane active" id="v{{vehicle.id}}_c3">
|
||||||
|
@ -97,6 +115,30 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -14,13 +14,29 @@ class VehicleStats:
|
||||||
self.litres = []
|
self.litres = []
|
||||||
self.average_litres = []
|
self.average_litres = []
|
||||||
self.odometers = []
|
self.odometers = []
|
||||||
|
self.costsPerLitre = []
|
||||||
|
self.costs = []
|
||||||
|
self.overall_costs = 0
|
||||||
|
self.average_costs_per_litre = 0
|
||||||
|
cost_count = 0;
|
||||||
|
|
||||||
if self.pitstop_count > 0:
|
if self.pitstop_count > 0:
|
||||||
for pitstop in vehicle.pitstops:
|
for pitstop in vehicle.pitstops:
|
||||||
self.overall_litres += pitstop.litres
|
self.overall_litres += pitstop.litres
|
||||||
self.litres.append(StatsEvent(pitstop.date, pitstop.litres))
|
self.litres.append(StatsEvent(pitstop.date, pitstop.litres))
|
||||||
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
|
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
|
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:
|
if self.pitstop_count > 1:
|
||||||
self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
||||||
self.average_distance = self.overall_distance / (self.pitstop_count - 1)
|
self.average_distance = self.overall_distance / (self.pitstop_count - 1)
|
||||||
|
@ -28,7 +44,9 @@ class VehicleStats:
|
||||||
for index in range(1, self.pitstop_count):
|
for index in range(1, self.pitstop_count):
|
||||||
last_ps = vehicle.pitstops[index - 1]
|
last_ps = vehicle.pitstops[index - 1]
|
||||||
current_ps = vehicle.pitstops[index]
|
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:
|
class StatsEvent:
|
||||||
|
|
Loading…
Reference in New Issue