Django:我应该如何存储货币值?

66
我在这里遇到了一个范式问题。我不知道是否应该将货币存储为 Decimal(),还是应该将其存储为字符串并自行转换为十进制数。我的理由是: PayPal 需要保留 2 位小数,因此如果我有一个售价为 49 美元的产品,PayPal 希望看到 49.00 在电线上传输。Django 的 DecimalField() 不设置十进制金额,它只存储最大小数位数。因此,如果您在里面放了49,并将字段设置为2个小数位,它仍将以49存储。我知道 Django 在反序列化回从数据库中反序列化时基本上执行类型转换(因为数据库没有十进制字段),因此我不完全关心速度问题,而更关心这个问题的设计问题。我想做最好的可扩展性。或者,更好的方法是,是否有人知道如何配置一个 Django DecimalField(),以始终使用 TWO_PLACES 格式样式。

1
关于“因为数据库没有小数字段”的说法,微软SQL Server有“decimal”和“money”数据类型。详情请参考http://msdn.microsoft.com/en-us/library/aa258271%28v=sql.80%29.aspx。 - Tim Abell
10个回答

65

您可能希望使用.quantize()方法。这个方法会将一个十进制数值四舍五入到特定的小数位数,您提供的参数指定了小数位数:

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

它还可以接受一个参数来指定你想要的舍入方式(不同的会计系统可能需要不同的舍入方式)。更多信息请参阅Python文档

以下是一个自定义字段,它可以自动产生正确的值。请注意,这仅适用于从数据库检索时,并且在您自己设置它时无法帮助您(直到将其保存到数据库并再次检索它!)。

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

[编辑]

添加了__metaclass__,请参见Django:为什么这个自定义模型字段的行为不如预期?


太好了,谢谢whrde。这就是你需要做的所有步骤来拥有一个完全功能的自定义模型字段吗?(即,除了格式化之外,它是否与DecimalField完全相同)。 - orokusaki
1
另外,存储自定义字段的惯例是什么?我想我会将其放在根项目文件夹中。 - orokusaki
谢谢,我刚用了这个。我注意到每次使用CurrencyField时都需要重复max_digits和decimal_places属性,所以我发布了一个答案来解决这个问题。 - Dave Aaron Smith
我希望你能解释一下 quantize 的原理,因为你的回答和 Python 文档都没有让我清楚地了解它的用途。 - user67416
1
好的,我已经为.quantize()添加了一个快速说明。 - Will Hardy
更新于Django 1.8 - 现在使用from_db_value()函数。 - AncientSwordRage

18

我认为你应该将它存储在十进制格式中,并将其格式化为00.00格式,仅当将其发送到PayPal时,像这样:

pricestr = "%01.2f" % price

如果你想的话,可以向你的模型中添加一个方法:

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

7
同时:将货币存储到数据库中,以及存储其金额。货币不仅仅是一个数字。 - Mr. Shiny and New 安宇
没错。谢谢Shiny和New先生。 - orokusaki
6
Shiny先生,如果您知道所有货币都是相同的话,就不需要在数据库中存储货币。实际上,大多数应用程序都不涉及多种货币。 - Will Hardy

13

我晚到派对的版本,增加了 South 迁移。

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'])

“['^application.fields.CurrencyField']” 中的路径是否需要根据我的项目进行更改?谢谢。 - pymarco

12

货币应该存储在货币字段中,可惜它并不存在。因为货币是二维值(金额、货币类型)。

有一个名为python-money的库,有许多分支,但我还没有找到可用的分支。


建议:

python-money可能是最好的分支:https://bitbucket.org/acoobe/python-money

django-money被akumria推荐:http://pypi.python.org/pypi/django-money/(我还没有尝试过这个)。


1
https://github.com/django-money/django-money 是当前的、维护良好的项目,并且可以通过 pip 安装。 - aris

10

我建议避免将表示与存储混合在一起。将数据作为带有2个小数位的十进制值进行存储。

在用户界面层,以适合用户的形式显示它(也许省略".00")。

当您将数据发送到PayPal时,请按照接口要求格式化它。


4

在@Will_Hardy的回答基础上,这里提供一个方法,让你不必每次都指定max_digits和decimal_places:

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

我有一个类似的字段,但我使用 defaults = {...defaults.update(**kwargs) 模式。 - orokusaki

4

从我的经验和其他人的经验来看,最好将货币存储为货币和以分为单位的金额的组合。

这样非常容易处理和计算。


请注意,并非每种货币都是1个较小单位(例如分)等于0.01个较大单位(美元)。 - ElmoVanKielmo
1
我不应该使用“分”的术语,而应该使用该货币的最小不可再分单元。 - Andre Bossard
然而,这种方式无法同时支持主要货币和次要货币之间汇率不同的货币。 - ElmoVanKielmo
@ElmoVanKielmo,你能给出这样一种货币的例子吗?比如说一种货币有1000个次要单位对应一个主要单位(1个次要单位=0.001个主要单位),我们可以将货币精度存储为元数据。 - pjotr_dolphin
@pjotr_dolphin 例如1枚硬币=0.001日元。您是正确的,货币精度应该作为元数据存储,正是出于这个原因。 - ElmoVanKielmo

1

你可以将它存储为 DecimalField,并在需要时手动添加小数,就像 Valya 所说的那样,使用基本的格式化技术。

你甚至可以在你的产品或交易模型中添加一个模型方法,将 DecimalField 输出为适当格式的字符串。


0

我是Django编程的第一线实践者,也涉及存储货币价值。

另外一种存储货币价值的方式是:将货币价值转换为其较小的表示形式,并将整数值存储在数据库中。如果数据库后端的舍入可调整为货币算术需求,则应该可以使用此方法。

在检索值时,只需将其读回并根据需要表示为字符串,通常是在将整数转换为字符串后在正确位置放置十进制分隔符。

看起来某些中间件恰好期望这种行为在不失去货币价值的情况下将小数转换为整数


0
更好的想法是将钱存储在较小的货币单位(可能的最小单位)中。对于欧元和美元,最小的货币单位是分。基于这个概念,有一个名为valuta的Python(和Django)包。 直接使用货币类
import valuta

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

使用(ISO-4217)货币代码的字符串表示进行编程

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集成

模型定义

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"])

样本数据

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,
)

使用魔术方法将小货币单位(分)转换为大货币单位(欧元)。
product.price_in_currency_units()  # Price in euros
# 10.0
product.price_with_tax_in_currency_units()  # Price in euros
# 12.0

如果您需要在模板中显示它:
product.price_display_in_currency_units()  # Price in euros
# '10.00'
product.price_with_tax_display_in_currency_units()  # Price in euros
# '12.00'

现在尝试另一种货币(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'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接