3. Money Types

Until django-SHOP version 0.2, amounts relating to money were kept inside a Decimal type and stored in the database model using a DecimalField. In shop installations with only one available currency, this wasn’t a major issue, because the currency symbol could be hard-coded anywhere on the site.

However, for sites offering pricing information in more than one currency, this caused major problems. When we needed to perform calculations with amounts that have an associated currency, it is very common to make mistakes by mixing different currencies. It also is common to perform incorrect conversions that generate wrong results. Python doesn’t allow developers to associate a specific decimal value with a unit.

Starting with version 0.9, django-SHOP now is shipped with a special factory class:

3.1. MoneyMaker

This class can not be instantiated, but is a factory for building a money type with an associated currency. Internally it uses the well established Decimal type to keep track of the amount. Additionally, it restricts operations on the current Money type. For instance, we can’t sum up Dollars with Euros. We also can’t multiply two currencies with each other.

3.1.1. Not a Number

In special occurrences we’d rather want to specify “no amount” rather than an amount of 0.00 (zero). This can be useful for free samples, or when an item is currently not available. The Decimal type denotes a kind of special value a NaN – for “Not a Number”. Our Money type also knows about this special value, and when rendered, or $ –`` is printed out.

Declaring a Money object without a value, say m = Money() creates such a special value. The big difference as for the Decimal type is that when adding or subtracting a NaN to a valid value, it is considered zero, rather than changing the result of this operation to NaN as well.

It also allows us to multiply a Money amount with None. The result of this operation is NaN.

3.1.2. Create a Money type

>>> from shop.money import MoneyMaker
>>> Money = MoneyMaker()
>>> print(Money('1.99'))
€ 1.99

>>> print(Money('1.55') + Money('8'))
€ 9.55

>>> print Money
<class 'shop.money.money_maker.MoneyInEUR'>

>>> Yen = MoneyMaker('JPY')
>>> print(Yen('1234.5678'))
¥ 1235

>>> print(Money('100') + Yen('1000'))
ValueError: Can not add/substract money in different currencies.

How does this work?

By calling MoneyMaker() a type accepting amounts in the default currency is created. The default currency can be changed in settings.py with SHOP_DEFAULT_CURRENCY = 'USD', using one of the official ISO-4217 currency codes.

Alternatively, we can create our own Money type, for instance Yen.

3.1.3. Formating Money

When the amount of a money type is printed or forced to text using str(price), it is prefixed by the currency symbol. This is fine, when working with only a few currencies. However, some symbols are ambiguous, for instance Canadian, Australian and US Dollars, which all use the “$” symbol.

With the setting SHOP_MONEY_FORMAT we can style how money is going to be printed out. This setting defaults to {symbol} {amount}. The following format strings are allowed:

  • {symbol}: The short symbol for a currency, for instance $, £, , ¥, etc.
  • {code}: The international currency code, for instance USD, GBP, EUR, JPY, etc.
  • {currency}: The spoken currency description, for instance “US Dollar”, “Pound Sterling”, etc.
  • {amount}: The amount, unlocalized.

Thus, if we prefer to print 9.98 US Dollar, then we should set {amount} {currency} as the formatting string.

3.2. Localizing Money

Since the Money class doesn’t know anything about our current locale setting, amounts always are printed unlocalized. To localize a Money type, use django.utils.numberformat.format(someamount). This function will return the amount, localized according to the current HTTP request.

3.3. Money Database Fields

Money can be stored in the database, keeping the currency information together with the field type. Internally, the database uses the Decimal type, but such a field knows its currency and will return an amount as MoneyIn... type. This prevents implicit, but accidental currency conversions.

In our database model, declare a field as:

class Product(models.Model):
    ...
    unit_price = MoneyField(currency='GBP')

This field stores its amounts as British Pounds and returns them typed as MoneyInGBP. If the currency argument is omitted, then the default currency is used.

3.4. Money Representation in JSON

An additional REST SerializerField has been added to convert amounts into JSON strings. When writing REST serializers, use:

from rest_framework import serializers
from shop.money.rest import MoneyField

class SomeSerializer(serializers.ModelSerializer):
    price = MoneyField()

The default REST behavior serializes Decimal types as floats. This is fine if we want to do some computations in the browser using JavaScript. However, then the currency information is lost and must be re-added somehow to the output strings. It also is a bad idea to do commercial calculations using floats, yet JavaScript does not offer any Decimal-like type. It therefore is recommended to always do the finance arithmetic on the server and transfer amount information using JSON strings.