"""
The latest version of this package is available at:
<http://github.com/jantman/biweeklybudget>
################################################################################
Copyright 2017 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
This file is part of biweeklybudget, also known as biweeklybudget.
biweeklybudget is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
biweeklybudget is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with biweeklybudget. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/biweeklybudget> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################
AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""
import logging
from sqlalchemy import (
Column, Integer, String, Boolean, Date, ForeignKey, SmallInteger, Numeric,
desc, inspect
)
from decimal import Decimal, ROUND_FLOOR
from sqlalchemy.orm import relationship, validates
from biweeklybudget.models.base import Base, ModelAsDict
from biweeklybudget.utils import dtnow
logger = logging.getLogger(__name__)
[docs]class Vehicle(Base, ModelAsDict):
__tablename__ = 'vehicles'
__table_args__ = (
{'mysql_engine': 'InnoDB'}
)
#: Primary Key
id = Column(Integer, primary_key=True)
#: Name of vehicle
name = Column(String(254))
#: whether active or historical
is_active = Column(Boolean, default=True)
def __repr__(self):
return "<Vehicle(id=%s, name=%s)>" % (
self.id, self.name
)
[docs]class FuelFill(Base, ModelAsDict):
__tablename__ = 'fuellog'
__table_args__ = (
{'mysql_engine': 'InnoDB'}
)
#: Primary Key
id = Column(Integer, primary_key=True)
#: date of the fill
date = Column(Date, default=dtnow().date())
#: ID of the vehicle
vehicle_id = Column(Integer, ForeignKey('vehicles.id'))
#: The vehicle
vehicle = relationship(
"Vehicle", backref="fuellog", uselist=False
)
#: Odometer reading of the vehicle, in miles
odometer_miles = Column(Integer)
#: Number of miles the vehicle thinks it's traveled since the last fill.
reported_miles = Column(SmallInteger)
#: Number of miles actually traveled since the last fill.
calculated_miles = Column(SmallInteger)
#: Fuel level before fill, as a percentage (Integer 0-100)
level_before = Column(SmallInteger)
#: Fuel level after fill, as a percentage (Integer 0-100)
level_after = Column(SmallInteger)
#: Location of fill - usually a gas station name/address
fill_location = Column(String(254))
#: Fuel cost per gallon
cost_per_gallon = Column(Numeric(precision=10, scale=4))
#: Total cost of fill
total_cost = Column(Numeric(precision=10, scale=4))
#: Total amount of fuel (gallons)
gallons = Column(Numeric(precision=10, scale=4))
#: MPG as reported by the vehicle itself
reported_mpg = Column(Numeric(precision=10, scale=4))
#: Calculated MPG, based on last fill
calculated_mpg = Column(Numeric(precision=10, scale=4))
#: Notes
notes = Column(String(254))
def __repr__(self):
return "<FuelFill(id=%s, vehicle=%s, date=%s)>" % (
self.id, self.vehicle_id, self.date
)
[docs] @validates('gallons')
def validate_gallons(self, _, value):
assert value > 0
return value
[docs] @validates('odometer_miles')
def validate_odometer_miles(self, _, value):
prev = self._previous_entry()
if prev is None:
logger.warning(
'Previous fill is None; cannot validate odometer_miles'
)
return value
assert self.odometer_miles > prev.odometer_miles
return value
[docs] def _previous_entry(self):
"""
Get the previous fill for this vehicle by odometer reading, or None.
:return: the previous fill for this vehicle, by odometer reading, or
None.
:rtype: biweeklybudget.models.fuel.FuelFill
"""
if self.vehicle is None:
logger.warning(
'vehicle is None; cannot obtain previous fill for %s', self
)
return None
if self.odometer_miles is None:
logger.warning(
'odometer_miles is None; cannot obtain previous fill for %s',
self
)
return None
return inspect(self).session.query(FuelFill).filter(
FuelFill.vehicle.__eq__(self.vehicle),
FuelFill.odometer_miles.__lt__(self.odometer_miles)
).order_by(
desc(FuelFill.odometer_miles)
).first()
[docs] def calculate_mpg(self):
"""
Calculate ``calculated_mpg`` field.
:returns: True if recalculate, False if unable to calculate
:rtype: bool
"""
if self.gallons is None:
logger.warning(
'Gallons is none; cannot recalculate MPG for %s', self
)
return False
if self.odometer_miles is None:
logger.warning(
'odometer_miles is none; cannot recalculate MPG for %s', self
)
return False
prev = self._previous_entry()
if prev is None:
logger.warning('Previous entry is None; cannot recalculate MPG '
'for %s', self)
return False
distance = self.odometer_miles - prev.odometer_miles
self.calculated_miles = distance
self.calculated_mpg = (
(distance * Decimal(1.0)) / self.gallons
).quantize(Decimal('.001'), rounding=ROUND_FLOOR)
logger.debug('Calculate MPG for fill %d: distance=%s mpg=%s',
self.id, distance, self.calculated_mpg)
inspect(self).session.add(self)