65

I'm running into a paradigm problem here. I don't know whether I should store money as a Decimal(), or if I should store it as a string and convert it to a decimal myself. My reasoning is this:

PayPal requires 2 decimal places, so if I have a product that is 49 dollars even, PayPal wants to see 49.00 come across the wire. Django's DecimalField() doesn't set a decimal amount. It only stores a maximum decimal places amount. So, if you have 49 in there, and you have the field set to 2 decimal places, it'll still store it as 49. I know that Django is basically type casting when it deserializes back from the database into a Decimal (since Databases don't have decimal fields), so I'm not completely concerned with the speed issues as much as I am with the design issues of this problem. I want to do what's best for extensibility.

Or, better yet, does anyone know how to configure a django DecimalField() to always format with the TWO_PLACES formatting style.

orokusaki
  • 48,267
  • 47
  • 159
  • 244
  • 1
    re "since Databases don't have decimal fields"; Microsoft Sql Server has both "decimal" and "money" data types http://msdn.microsoft.com/en-us/library/aa258271%28v=sql.80%29.aspx – Tim Abell May 16 '11 at 20:47

9 Answers9

64

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?

Community
  • 1
  • 1
Will Hardy
  • 13,389
  • 5
  • 40
  • 41
  • Excellent. Thanks whrde. Is this all you have to do to have a fully functional custom model field? (ie, will this work exactly the same as DecimalField except for the formatting). – orokusaki Jan 08 '10 at 14:15
  • 1
    Also, what is the convention for storing custom fields? I'm thinking I'll put it in the root project folder. – orokusaki Jan 08 '10 at 14:16
  • Thanks, I just used this. I noticed I had to repeat the max_digits and decimal_places attributes every time I used CurrencyField, so I posted an answer that builds on yours to addresses this. – Dave Aaron Smith Nov 01 '12 at 21:44
  • I wish you'd explain the rationale for `quantize` because neither your answer nor the python documentation makes that use clear to me. – g33kz0r Jun 08 '13 at 21:50
  • 1
    Ok, I've added a quick explanation for `.quantize()` – Will Hardy Jun 11 '13 at 12:50
  • Updated in [Django 1.8](https://docs.djangoproject.com/en/1.8/howto/custom-model-fields/#converting-values-to-python-objects) - Now use from_db_value() – Pureferret May 25 '15 at 19:04
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
Valentin Golev
  • 9,653
  • 8
  • 57
  • 79
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_
  • 354
  • 3
  • 10
  • Does the path in "['^application\.fields\.CurrencyField']" have to be changed according to my project? Thx – pymarco Dec 05 '13 at 22:40
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
  • 7,544
  • 5
  • 28
  • 40
  • 1
    https://github.com/django-money/django-money is current, maintained and installs via pip – aris Jan 01 '19 at 23:21
  • After trying to use it, I'll warn you that it's not really maintained (I had the issue that you [can't set the field as nullable and without using a default currency](https://github.com/django-money/django-money/issues/530) and `makemigrations` would [constantly generate a new migration](https://github.com/django-money/django-money/issues/106) and it depends on py-moneyed, which is also not really maintained: https://github.com/limist/py-moneyed/issues/22#issuecomment-447059971 – Boris Jul 29 '20 at 01:47
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
  • 297,790
  • 101
  • 558
  • 777
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
Dave Aaron Smith
  • 4,349
  • 29
  • 37
  • I have a field similar to that, but I use the `defaults = {...` and `defaults.update(**kwargs)` pattern. – orokusaki Nov 02 '12 at 18:00
3

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
  • 6,042
  • 30
  • 51
  • Be aware that it's not true for every currency that 1 minor (e.g. cent) equals 0.01 major (dollar). – ElmoVanKielmo May 14 '15 at 07:22
  • I should not have used the term "cents", but more the smallest no further divisible unit of that currency. – Andre Bossard May 22 '15 at 12:41
  • Still, this way your app won't support currencies with different ratio between major and minor at the same time. – ElmoVanKielmo May 22 '15 at 13:05
  • @ElmoVanKielmo can you give an example of such a currency? Lets say one currency have 1000 minor for one major (1minor=0.001major), one could just store the currency precision as meta data. – pjotr_dolphin Mar 15 '17 at 09:18
  • @pjotr_dolphin Japanese currency for example 1 rin = 0.001 yen. And you are right that currency precision should be stored as meta data exactly for this reason – ElmoVanKielmo Mar 15 '17 at 12:17
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
  • 6,674
  • 9
  • 48
  • 74
-1

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
  • 1