Money database property for Google App Engine
Calculating money is a tricky thing. Your calculations have to be ultra-precise, so no storing things as floats where $1.33 might actually be stored as 1.3299999999. That is no good for calculations… But equally it is no good storing in cents either: what is 133c /2?
Python has a Decimal datatype, but this requires serialisation to String. Nick Johnson from the Google App Engine team wrote a post about how to write a Decimal property for Google App Engine. Unfortunately this because this serialises to String, then when you do any sorting, then you get String sorting: 100 comes before 11, which comes before 20. Bummer. I played around with storing numbers with a bunch of leading zeroes ie: 0000000010. But that starts to feel a bit hacky.
So I wrote a Money database property that has 6 places of precision, and works as a normal Python numeric type. I haven’t implemented all of those methods, just the ones that I needed. I would be happy to take feedback though.
To use:
class Transaction(db.model): dateOccurred = db.DateProperty(auto_now_add=True) description = db.StringProperty(required=True) amount = MoneyProperty(required=True) t = Transaction(description="I got some money", amount=Money(10.34))
from google.appengine.ext import db class Money(object): multiple = 1000000.0 def __init__(self, val, multiply=True): if multiply: self._intVal = int(float(val) * self.multiple) else: self._intVal = int(val) def format(self, places=2): return "%.*f" % (places, float(self)) def __float__(self): return self._intVal / self.multiple def __repr__(self): return "%.06f" % (self._intVal / self.multiple) def __mul__(self, other): if type(other) == Money: return Money((self._intVal * other._intVal) / self.multiple, False) else: return Money(self._intVal * other, False) __rmul__ = __mul__ def __add__(self, other): if type(other) == Money: return Money(self._intVal + other._intVal, False) else: return Money(self._intVal + (other * self.multiple), False) __radd__ = __add__ def __cmp__(self, other): if other == None: return 1 elif other == "": return 1 elif type(other) != Money: return self._intVal - other*self.multiple return self._intVal - other._intVal def __sub__(self, other): return Money(self._intVal - other._intVal, False) def __rsub__(self, other): return Money(other._intVal - self._intVal, False) def __div__(self, other): if type(other) == Money: return Money((self._intVal * self.multiple) / other._intVal, False) else: return Money(self._intVal / other, False) def __rdiv__(self, other): if type(other) == Money: return Money((other._intVal * self.multiple) / self._intVal, False) else: return Money((other * self.multiple * self.multiple) / self._intVal, False) def __neg__(self): return Money(self._intVal * -1, False) class MoneyProperty(db.Property): data_type = Money def get_value_for_datastore(self, model_instance): value = super(MoneyProperty, self).get_value_for_datastore(model_instance) if value==None: return None elif isinstance(value, Money): return value._intVal else: return Money(value)._intVal def make_value_from_datastore(self, value): if value==None: return None else: return Money(value, False) def empty(self, value): return value == None def get_value_for_form(self, instance): value = super(MoneyProperty, self).get_value_for_form(instance) if not value: return None if isinstance(value, Money): return float(value) return value def make_value_from_form(self, value): if not value: return [] if isinstance(value, Money): return Money(value) return value