[Django]-Django: How should I store a money value?

65πŸ‘

βœ…

You might want to use the .quantize() method. This will round a decimal value to a certain number of places, the argument you provide specifies the number of places:

>>> from decimal import Decimal
>>> Decimal("12.234").quantize(Decimal("0.00"))
Decimal("12.23")

It can also take an argument to specify what rounding approach you want (different accounting systems might want different rounding). More info in the Python docs.

Below is a custom field that automatically produces the correct value. Note that this is only when it is retrieved from the database, and wont help you when you set it yourself (until you save it to the db and retrieve it again!).

from django.db import models
from decimal import Decimal
class CurrencyField(models.DecimalField):
    __metaclass__ = models.SubfieldBase

    def to_python(self, value):
        try:
           return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
        except AttributeError:
           return None

[edit]

added __metaclass__, see Django: Why does this custom model field not behave as expected?

πŸ‘€Will Hardy

18πŸ‘

I think you should store it in a decimal format and format it to 00.00 format only then sending it to PayPal, like this:

pricestr = "%01.2f" % price

If you want, you can add a method to your model:

def formattedprice(self):
    return "%01.2f" % self.price

13πŸ‘

My late to the party version that adds South migrations.

from decimal import Decimal
from django.db import models

try:
    from south.modelsinspector import add_introspection_rules
except ImportError:
    SOUTH = False
else:
    SOUTH = True

class CurrencyField(models.DecimalField):
    __metaclass__ = models.SubfieldBase

    def __init__(self, verbose_name=None, name=None, **kwargs):
        decimal_places = kwargs.pop('decimal_places', 2)
        max_digits = kwargs.pop('max_digits', 10)

        super(CurrencyField, self). __init__(
            verbose_name=verbose_name, name=name, max_digits=max_digits,
            decimal_places=decimal_places, **kwargs)

    def to_python(self, value):
        try:
            return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
        except AttributeError:
            return None

if SOUTH:
    add_introspection_rules([
        (
            [CurrencyField],
            [],
            {
                "decimal_places": ["decimal_places", { "default": "2" }],
                "max_digits": ["max_digits", { "default": "10" }],
            },
        ),
    ], ['^application\.fields\.CurrencyField'])
πŸ‘€so_

12πŸ‘

Money should be stored in money field, which sadly does not exist. Since money is two dimensional value (amount, currency).

There is python-money lib, that has many forks, yet I haven’t found working one.


Recommendations:

python-money probably the best fork https://bitbucket.org/acoobe/python-money

django-money recommended by akumria: http://pypi.python.org/pypi/django-money/ (havent tried that one yet).

πŸ‘€aisbaa

10πŸ‘

I suggest to avoid mixing representation with storage. Store the data as a decimal value with 2 places.

In the UI layer, display it in a form which is suitable for the user (so maybe omit the β€œ.00”).

When you send the data to PayPal, format it as the interface requires.

πŸ‘€Aaron Digulla

4πŸ‘

In my experience and also from others, money is best stored as combination of currency and the amount in cents.

It’s very easy to handle and calculate with it.

πŸ‘€Andre Bossard

4πŸ‘

Building on @Will_Hardy’s answer, here it is so you don’t have to specify max_digits and decimal_places every time:

from django.db import models
from decimal import Decimal


class CurrencyField(models.DecimalField):
  __metaclass__ = models.SubfieldBase

  def __init__(self, verbose_name=None, name=None, **kwargs):
    super(CurrencyField, self). __init__(
        verbose_name=verbose_name, name=name, max_digits=10,
        decimal_places=2, **kwargs)

  def to_python(self, value):
    try:
      return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
    except AttributeError:
      return None

1πŸ‘

You store it as a DecimalField and manually add the decimals if you need to, as Valya said, using basic formatting techniques.

You can even add a Model Method to you product or transaction model that will spit out the DecimalField as an appropriately formatted string.

πŸ‘€M. Ryan

0πŸ‘

I’m first-hands-on on Django and storing money values, too.

There is another possibility to store money values: just convert a money value to its minor representation and store that integer value in the database. If the rounding of the db backend can be adjusted to monetary arithmetic needs, this should work.

When retrieving a value, just read it back and represent it as a string as needed, which usually involves placing a decimal separator at the right position after converting the integer to a string.

It looks that some middleware exactly expects such behaviour Convert decimal to integer without losing monetary value.

πŸ‘€tdk

0πŸ‘

A better idea is to store money in minor currency units (smallest units possible). For euros and dollars the minor currency units are cents. There’s a Python (and Django) package called valuta based on the concept.

Using currency classes directly

import valuta

valuta.EUR.convert_to_currency_units(1_000)  # 1_000 is the price in cents
# 10.0

Working with string representations of the (ISO-4217) currency codes

from valuta.shortcuts import convert_to_currency_units

convert_to_currency_units("EUR", 1_000)  # 1_000 is the price in cents
# 10.0

Django integration

Model definition

from django.db import models
from valuta.contrib.django_integration.models import CurrencyField

class Product(models.Model):

    price = models.IntegerField()  # Amount in minor currency units
    price_with_tax = models.IntegerField()  # Amount in minor currency units
    currency = CurrencyField(amount_fields=["price", "price_with_tax"])

Sample data

import valuta
from product.models import Product
product = Product.objects.create(
    price=1_000,  # Price in cents
    price_with_tax=1_200,  # Price in cents
    currency=valuta.EUR.uid,
)

Use magic methods to convert from minor currency (cents) units to major currency units (euros)

product.price_in_currency_units()  # Price in euros
# 10.0
product.price_with_tax_in_currency_units()  # Price in euros
# 12.0

If you need to display it in templates:

product.price_display_in_currency_units()  # Price in euros
# '10.00'
product.price_with_tax_display_in_currency_units()  # Price in euros
# '12.00'

Now try another currency (JPY).

product_jpy = Product.objects.create(
    price=1_000,  # Price in cents
    price_with_tax=1_200,  # Price in cents
    currency=valuta.JPY.uid,
)

product.price_in_currency_units()  # Price in JPY
# 10.0
product.price_with_tax_in_currency_units()  # Price in JPY
# 12.0

product.price_display_in_currency_units()  # Price in JPY
# '10'
product.price_with_tax_display_in_currency_units()  # Price in JPY
# '12'

Leave a comment