Keeping accurate amounts in Django with CurrencyField

At Wundermart we started saving monetary amounts in Django, we naively started with DecimalField(decimal_places=2) for our financial data. After some time we realized that you will lose data this way when applying calculations, for example:

€100 * 0.21 VAT = € 82.644628099

When you save this to the database it will round to 2 decimal places. If you then reapply the same VAT again you notice you've lost a cent:

€ 82.64 * 1.21 VAT = € 99.9944 = € 99.99

However, when your field allows for 4 decimal places to be stored in the database, you will have enough information to round exactly to what you started with:

€ 82.6446 * 1.21 VAT = € 99.999966 = € 100.00

The calculation above is done in Python this way:

Decimal('99.999966').quantize(Decimal('0.00'), rounding='ROUND_HALF_EVEN')

The first argument of quantize determines how many decimals you want to round to.

ROUND_HALF_EVEN is called Banker's rounding and is considered today's standard for rounding monetary values since it's the least biased form of rounding. Though it has to be said that there does not seem to be one uniform standard way for dealing with money, local legislation might be different.

Putting it in a custom DecimalField

This subclass of DecimalField will enforce all fields that process money to have the same 4-decimal accuracy in the database. Meanwhile, when the field is read-only, it will show 2 decimals for representation.

Note that it will still show 4 decimals when editing in a form, if you'd round this to 2 the same problem will start happening. So it's imperative to keep the decimals in the database as-is and only round them when in the end representing it to the user.

For our situation 4 decimals will always be enough, it could be that your calculations or use-case would require an even higher accuracy. This can be set with db_decimals.

Bonus: serializing currency with Django Rest Framework

When not editing, CurrencyField will show 2 decimals in the admin forms. However, when you use Django Rest Framework the serialized output in APIs will still show 4 decimals. This might not be what you want, therefore you can use this customized DRF field:

To show two decimals for a field in the API, use it like this:

class ArticleSerializer(serializers.ModelSerializer):
    sales_price = CurrencySerializerField(required=False)

Feel free to modify these examples to your needs.