Skip to main content

添加对在 django 模型和表单中使用货币和货币字段的支持。使用 py-moneyed 作为货币实现。

项目描述

构建状态 覆盖状态 文件状态 派皮

一个小 Django 应用程序,它使用py-moneyed在模型和表单中添加对 Money 字段的支持。

  • 支持的 Django 版本:2.2、3.2、4.0、4.1

  • 支持的 Python 版本:3.7、3.8、3.9、3.10

  • 支持的 PyPy 版本:PyPy3

如果您需要对旧版本 Django 和 Python 的支持,请参阅发行说明中提到的旧版本。

通过依赖py-moneyeddjango-money得到:

  • 支持正确的 Money 值处理(使用标准 Money 设计模式)

  • 流通中所有货币的货币类别和定义

  • 使用正确的货币符号格式化大多数货币

安装

使用点子

$ pip install django-money

这会自动安装py-moneyed v1.2(或更高版本)。

djmoney添加到您的INSTALLED_APPS中。这是必需的,以便在管理员中正确显示货币字段。

INSTALLED_APPS = [
   ...,
   'djmoney',
   ...
]

模型使用

用作普通模型字段:

from djmoney.models.fields import MoneyField
from django.db import models


class BankAccount(models.Model):
    balance = MoneyField(max_digits=14, decimal_places=2, default_currency='USD')

为了遵守某些严格的会计或财务法规,您可以考虑使用max_digits=19和decimal_places =4,请参阅此StackOverflow 答案中的更多信息

也可以有一个可为空的MoneyField

class BankAccount(models.Model):
    money = MoneyField(max_digits=10, decimal_places=2, null=True, default_currency=None)

account = BankAccount.objects.create()
assert account.money is None
assert account.money_currency is None

搜索带有货币字段的模型:

from djmoney.money import Money


account = BankAccount.objects.create(balance=Money(10, 'USD'))
swissAccount = BankAccount.objects.create(balance=Money(10, 'CHF'))

BankAccount.objects.filter(balance__gt=Money(1, 'USD'))
# Returns the "account" object

默认货币代码长度为3,但您可以使用CURRENCY_CODE_MAX_LENGTH设置更改它。

注意:此设置也会影响交换插件的初始迁移,因此在运行初始迁移后更改它没有效果。(如果您想更改它,您需要管理迁移交换零并再次迁移)。

现场验证

字段验证有 3 种不同的可能性:

  • 尽管有货币,但按货币的数字部分;

  • 按单笔金额;

  • 由多个金额。

它们都可以组合使用,如下所示:

from django.db import models
from djmoney.models.fields import MoneyField
from djmoney.money import Money
from djmoney.models.validators import MaxMoneyValidator, MinMoneyValidator


class BankAccount(models.Model):
    balance = MoneyField(
        max_digits=10,
        decimal_places=2,
        validators=[
            MinMoneyValidator(10),
            MaxMoneyValidator(1500),
            MinMoneyValidator(Money(500, 'NOK')),
            MaxMoneyValidator(Money(900, 'NOK')),
            MinMoneyValidator({'EUR': 100, 'USD': 50}),
            MaxMoneyValidator({'EUR': 1000, 'USD': 500}),
        ]
    )

上述模型中的balance字段具有以下验证:

  • 所有输入值应在 10 到 1500 之间,尽管使用货币;

  • 挪威克朗金额 (NOK) 应在 500 到 900 之间;

  • 欧元应该在100到1000之间;

  • 美元应该在50到500之间;

添加新货币

货币列在 moneyed 上,此模块使用它在管理员上提供选择列表,也用于验证。

要为所有项目添加可用的新货币,您只需将以下几行添加到您的settings.py文件中:

import moneyed

BOB = moneyed.add_currency(
    code='BOB',
    numeric='068',
    name='Peso boliviano',
    countries=('BOLIVIA', )
)

要限制项目中列出的货币,请在 settings.py 上设置一个带有货币代码列表的CURRENCIES 变量

CURRENCIES = ('USD', 'BOB')

该列表必须包含有效的货币代码

此外,还可以直接指定货币选择:

CURRENCIES = ('USD', 'EUR')
CURRENCY_CHOICES = [('USD', 'USD $'), ('EUR', 'EUR €')]

关于模型管理器的重要说明

Django-money 让您可以为模型使用任何您喜欢的自定义模型管理器,但它需要包装一些方法以允许搜索具有货币值的模型。

对于使用 MoneyField 的任何模型中的“对象”属性,这是自动完成的。但是,如果将管理器分配给其他属性,则必须手动包装管理器,如下所示:

from djmoney.models.managers import money_manager


class BankAccount(models.Model):
    balance = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
    accounts = money_manager(MyCustomManager())

此外,money_manager 包装器仅包装标准 QuerySet 方法。如果你定义了自定义的 QuerySet 方法,最终不使用任何标准方法(如“get”、“filter”等),那么你还需要手动装饰这些自定义方法,如下所示:

from djmoney.models.managers import understands_money


class MyCustomQuerySet(QuerySet):

   @understands_money
   def my_custom_method(*args, **kwargs):
       # Awesome stuff

格式本地化

如果您在设置文件中设置了USE_L10N = True ,则会打开格式设置。

如果在配置中禁用格式化,则在模板中将使用默认格式。

在模板中,您可以使用特殊标签来格式化货币。

在文件settings.py中,从库djmoney添加到INSTALLED_APPS条目:

INSTALLED_APPS += ('djmoney', )

在模板中,添加:

{% load djmoney %}
...
{% money_localize money %}

仅此而已。

对标签money_localize的说明:

{% money_localize <money_object> [ on(default) | off ] [as var_name] %}
{% money_localize <amount> <currency> [ on(default) | off ] [as var_name] %}

例子:

同样的效果:

{% money_localize money_object %}
{% money_localize money_object on %}

赋值给变量:

{% money_localize money_object on as NEW_MONEY_OBJECT %}

用货币格式化数字:

{% money_localize '4.5' 'USD' %}
Return::

    Money object

测试

安装所需的软件包:

git clone https://github.com/django-money/django-money

cd ./django-money/

pip install -e ".[test]" # installation with required packages for testing

运行测试的推荐方法:

tox

在当前环境python中测试应用程序:

make test

使用汇率

要使用汇率,请将以下内容添加到您的INSTALLED_APPS中。

INSTALLED_APPS = [
    ...,
    'djmoney.contrib.exchange',
]

此外,还需要安装证书。可以通过安装djmoney和额外的交换来完成:

pip install "django-money[exchange]"

要创建所需的关系,请运行python manage.py migrate。要使用数据填充这些关系,您需要选择一个数据源。目前,支持 2 个数据源 - https://openexchangerates.org/(默认)和https://fixer.io/。要选择另一个数据源,将带有可导入字符串的EXCHANGE_BACKEND设置设置到后端,您需要:

EXCHANGE_BACKEND = 'djmoney.contrib.exchange.backends.FixerBackend'

如果要实现自己的后端,则需要扩展djmoney.contrib.exchange.backends.base.BaseExchangeBackend。上面提到的两个数据源都没有开放,所以你必须指定访问密钥才能使用它们:

OPEN_EXCHANGE_RATES_APP_ID - '<您来自 openexchangerates.org 的实际密钥>'

FIXER_ACCESS_KEY - '<您来自 fixer.io 的实际密钥>'

基础货币的后端回报率,默认为美元,但可以通过BASE_CURRENCY设置进行更改。Open Exchanger Rates & Fixer 支持一些额外的东西,例如历史数据或限制货币以响应特定列表。为了使用这些功能,您可以更改这些后端的默认 URL:

OPEN_EXCHANGE_RATES_URL = 'https://openexchangerates.org/api/historical/2017-01-01.json?symbols=EUR,NOK,SEK,CZK'
FIXER_URL = 'http://data.fixer.io/api/2013-12-24?symbols=EUR,NOK,SEK,CZK'

或者,您可以将其直接传递给update_rates方法:

>>> from djmoney.contrib.exchange.backends import OpenExchangeRatesBackend
>>> backend = OpenExchangeRatesBackend(url='https://openexchangerates.org/api/historical/2017-01-01.json')
>>> backend.update_rates(symbols='EUR,NOK,SEK,CZK')

有可能同时使用多个后端:

>>> from djmoney.contrib.exchange.backends import FixerBackend, OpenExchangeRatesBackend
>>> from djmoney.contrib.exchange.models import get_rate
>>> OpenExchangeRatesBackend().update_rates()
>>> FixerBackend().update_rates()
>>> get_rate('USD', 'EUR', backend=OpenExchangeRatesBackend.name)
>>> get_rate('USD', 'EUR', backend=FixerBackend.name)

Money的常规操作将使用EXCHANGE_BACKEND后端来获取费率。此外,还有两个管理命令可用于更新和删除费率:

$ python manage.py update_rates
Successfully updated rates from openexchangerates.org
$ python manage.py clear_rates
Successfully cleared rates for openexchangerates.org

它们都接受-b/--backend选项,它将仅更新/清除此后端的数据。并且clear_rates接受-a/--all选项,这将清除所有后端的数据。

要设置定期费率更新,您可以使用 Celery 任务:

CELERYBEAT_SCHEDULE = {
    'update_rates': {
        'task': 'path.to.your.task',
        'schedule': crontab(minute=0, hour=0),
        'kwargs': {}  # For custom arguments
    }
}

示例任务实现:

from django.utils.module_loading import import_string

from celery import Celery
from djmoney import settings


app = Celery('tasks', broker='pyamqp://guest@localhost//')


@app.task
def update_rates(backend=settings.EXCHANGE_BACKEND, **kwargs):
    backend = import_string(backend)()
    backend.update_rates(**kwargs)

要将一种货币转换为另一种货币:

>>> from djmoney.money import Money
>>> from djmoney.contrib.exchange.models import convert_money
>>> convert_money(Money(100, 'EUR'), 'USD')
<Money: 122.8184375038380800 USD>

汇率与 Django Admin 集成。

通过在 Django 设置中设置AUTO_CONVERT_MONEY = True可以将 django-money 配置为自动使用此应用进行货币转换。请注意,货币转换是一个有损过程,因此自动转换通常仅适用于非常简单的用例。对于大多数用例,您需要明确何时发生货币转换,并且自动转换可以隐藏错误。此外,通过自动转换,由于发生在不同方向的转换,您会丢失一些属性,例如交换性 ( A + B == B + A )。

与 Django REST 框架一起使用

确保djmoney位于settings.py的INSTALLED_APPS中 ,并且已经安装了rest_framework 。MoneyField 将通过djmoney.apps.MoneyConfig.ready()自动为 Django REST Framework 注册一个序列化程序 。

您可以通过以下方式添加可序列化字段:

from djmoney.contrib.django_rest_framework import MoneyField

class Serializers(serializers.Serializer):
    my_computed_prop = MoneyField(max_digits=10, decimal_places=2)

内置序列化程序的工作方式如下:

class Expenses(models.Model):
    amount = MoneyField(max_digits=10, decimal_places=2)


class Serializer(serializers.ModelSerializer):
    class Meta:
        model = Expenses
        fields = '__all__'

>>> instance = Expenses.objects.create(amount=Money(10, 'EUR'))
>>> serializer = Serializer(instance=instance)
>>> serializer.data
ReturnDict([
    ('id', 1),
    ('amount_currency', 'EUR'),
    ('amount', '10.000'),
])

请注意,在您的序列化程序上指定单个字段时,金额和货币字段是分开处理的。要实现与上述相同的行为,您将包括两个字段名称:

class Serializer(serializers.ModelSerializer):
    class Meta:
        model = Expenses
        fields = ('id', 'amount', 'amount_currency')

定制

如果需要自定义将Money实例解构到 Django 字段上的过程,或者反过来,则可以使用像这样的自定义描述符:

class MyMoneyDescriptor:

    def __get__(self, obj, type=None):
        amount = obj.__dict__[self.field.name]
        return Money(amount, "EUR")

当调用 obj.money时,它将始终对所有Money实例使用EUR。然后它应该被传递给MoneyField

class Expenses(models.Model):
    amount = MoneyField(max_digits=10, decimal_places=2, money_descriptor_class=MyMoneyDescriptor)

背景

该项目是 http://code.google.com/p/python-money/中的 Django 支持的一个分支

这个版本增加了测试,并带有几个关键的错误修正。