pax_global_header00006660000000000000000000000064152112464620014515gustar00rootroot0000000000000052 comment=778f51df21a4ceee8d229e771c030b4ee288de00 django-money/000077500000000000000000000000001521124646200134505ustar00rootroot00000000000000django-money/.coveragerc000066400000000000000000000001521521124646200155670ustar00rootroot00000000000000[run] branch = true [report] show_missing = true precision = 2 exclude_lines = raise NotImplementedError django-money/.github/000077500000000000000000000000001521124646200150105ustar00rootroot00000000000000django-money/.github/SUPPORT.rst000066400000000000000000000010571521124646200167210ustar00rootroot00000000000000Support ======= If you are looking for help with django-money, please follow these steps: #. Read the `the documentation`_ #. Check `the list of current issues`_ to see if it is a known problem #. Ask your question on `Stack Overflow`_ .. _the documentation: http://django-money.readthedocs.io/ .. _the list of current issues: https://github.com/django-money/django-money/issues .. _Stack Overflow: https://stackoverflow.com/search?q=django-money **DO NOT OPEN ISSUES ASKING FOR SUPPORT:** This issue tracker is for reporting real issues in django-money. django-money/.github/workflows/000077500000000000000000000000001521124646200170455ustar00rootroot00000000000000django-money/.github/workflows/main.yml000066400000000000000000000045271521124646200205240ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: pre-commit: name: Generic pre-commit checks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: actions/setup-python@v4 with: python-version: '3.10' - uses: astral-sh/setup-uv@v5 - run: uv pip install --system pre-commit pre-commit-uv - run: pre-commit run --all-files docs: name: docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: actions/setup-python@v4 with: python-version: '3.10' - uses: astral-sh/setup-uv@v5 - run: uv pip install --system tox tox-uv - name: Run docs tox job run: tox -e docs tests: strategy: fail-fast: false matrix: include: - python: '3.10' tox_env: django42-py310 - python: '3.10' tox_env: django50-py310 - python: '3.10' tox_env: no_rest_framework - python: '3.11' tox_env: django50-py311 - python: '3.11' tox_env: django42-py311 - python: '3.11' tox_env: no_rest_framework - python: '3.11' tox_env: django51-py311 - python: '3.12' tox_env: django51-py312 - python: '3.13' tox_env: django51-py313 - python: '3.11' tox_env: django52-py311 - python: '3.12' tox_env: django52-py312 - python: '3.13' tox_env: django52-py313 - python: '3.12' tox_env: django_main-py312 - python: '3.13' tox_env: django_main-py313 name: ${{ matrix.tox_env }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - uses: astral-sh/setup-uv@v5 - run: uv pip install --system tox tox-uv coverage - name: Run ${{ matrix.tox_env }} tox job run: tox -e ${{ matrix.tox_env }} - run: coverage xml -i - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml name: codecov-py${{ matrix.python }} django-money/.github/workflows/release.yml000066400000000000000000000011121521124646200212030ustar00rootroot00000000000000name: Release on: release: types: [published] jobs: build-n-publish: name: Build and publish Python 🐍distributions 📦 to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 uses: actions/setup-python@v5 with: python-version: "3.10" - uses: astral-sh/setup-uv@v5 - name: Build a binary wheel and a source tarball run: uv build - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} django-money/.gitignore000066400000000000000000000004661521124646200154460ustar00rootroot00000000000000*.py[cod] # Packages *.eggs *.egg-info *.~ *.wpr # WingIDE *.wpu # WingIDE .installed.cfg bin develop-eggs dist downloads parts build .ropeproject .tox __pycache__ .project .pydevproject .settings .idea .venv .cache .pytest_cache docs/_build # Coverage reports .coverage .coverage.* coverage.xml htmlcov django-money/.pre-commit-config.yaml000066400000000000000000000020021521124646200177230ustar00rootroot00000000000000default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-yaml - id: check-added-large-files - id: debug-statements - id: end-of-file-fixer - repo: https://github.com/asottile/pyupgrade rev: v3.20.0 hooks: - id: pyupgrade args: - --py38-plus - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: - id: isort - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: - id: flake8 exclude: ^docs - repo: https://github.com/psf/black rev: 25.1.0 hooks: - id: black exclude: ^docs - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.1 hooks: - id: mypy additional_dependencies: - django-stubs[compatible-mypy] - py-moneyed files: 'djmoney\/.*\.py|tests\/.*\.py' exclude: djmoney/money.py django-money/.readthedocs.yaml000066400000000000000000000007111521124646200166760ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" jobs: install: - pip install --upgrade pip - pip install -e . --group docs # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py django-money/CONTRIBUTING.rst000066400000000000000000000026301521124646200161120ustar00rootroot00000000000000############################ Contributing to Django money ############################ Django-money is contribute-friendly project. Contributions are highly welcomed and appreciated. There is a guideline for contributing. Quickstart ---------- - All code contributions should be tested - Code should conform to syntax conventions. There's a ``tox`` command to help fixing it: ``tox -e fix-lint`` - Documentation should be updated if it is required - Put a note to the changelog. Syntax and conventions ---------------------- The source code should conform to `PEP8`_ with following notice: - Line length should not exceed **120** characters. Running the tests ----------------- We use ``tox`` to run the tests:: $ tox -e lint,django111-py36,django111-py27 The test environments above are usually enough to cover most cases locally. .. _PEP8: http://www.python.org/dev/peps/pep-0008/ Report bugs ----------- Report bugs in the `issue tracker `_. If you are reporting a bug, please include: * Any details about your local setup that might be helpful in troubleshooting, specifically the Python interpreter version, Django & django-money versions. * Detailed steps to reproduce the bug. If you can write a demonstration test that currently fails but should pass (xfail), that is a very useful commit to make as well, even if you cannot fix the bug itself. django-money/LICENSE.txt000066400000000000000000000033331521124646200152750ustar00rootroot00000000000000 = Jacob Hansson = Voltvoodoo = 2011 In the original BSD license, both occurrences of the phrase "COPYRIGHT HOLDERS AND CONTRIBUTORS" in the disclaimer read "REGENTS AND CONTRIBUTORS". Here is the license template: Copyright (c) , All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-money/MANIFEST.in000066400000000000000000000001271521124646200152060ustar00rootroot00000000000000include setup.py include LICENSE.txt include README.rst recursive-include djmoney *.py django-money/Makefile000066400000000000000000000021201521124646200151030ustar00rootroot00000000000000help: @echo "clean - remove all build, test, coverage and Python artifacts" @echo "clean-build - remove build artifacts" @echo "clean-pyc - remove Python file artifacts" @echo "clean-test - remove test and coverage artifacts" @echo "test - run tests quickly with the default Python" @echo "coverage - check code coverage quickly with the default Python" @echo "install - install the package to the active Python's site-packages" clean: clean-test clean-build clean-pyc clean-build: rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -fr {} + clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ test: pytest --cov=./djmoney tests coverage: test coverage report -m coverage html install: clean uv sync release: dist ## Generate and upload release to PyPi uv publish dist: clean ## Generate source dist and wheels uv build ls -l dist django-money/README.rst000066400000000000000000000455051521124646200151500ustar00rootroot00000000000000django-money ============ .. image:: https://github.com/django-money/django-money/workflows/CI/badge.svg :target: https://github.com/django-money/django-money/actions :alt: Build Status .. image:: http://codecov.io/github/django-money/django-money/coverage.svg?branch=main :target: http://codecov.io/github/django-money/django-money?branch=main :alt: Coverage Status .. image:: https://readthedocs.org/projects/django-money/badge/?version=latest :target: http://django-money.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/django-money.svg :target: https://pypi.python.org/pypi/django-money :alt: PyPI A little Django app that uses `py-moneyed `__ to add support for Money fields in your models and forms. * Django versions supported: 4.2, 5.0, 5.1, 5.2 * Python versions supported: 3.10, 3.11, 3.12, 3.13 * PyPy versions supported: PyPy3 If you need support for older versions of Django and Python, please refer to older releases mentioned in `the release notes `__. Through the dependency `py-moneyed `__, ``django-money`` gets: * Support for proper Money value handling (using the standard Money design pattern) * A currency class and definitions for all currencies in circulation * Formatting of most currencies with correct currency sign Installation ------------ Using `pip`: .. code:: bash $ pip install django-money This automatically installs ``py-moneyed`` v1.2 (or later). Add ``djmoney`` to your ``INSTALLED_APPS``. This is required so that money field are displayed correctly in the admin. .. code:: python INSTALLED_APPS = [ ..., 'djmoney', ... ] Model usage ----------- Use as normal model fields: .. code:: python 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') To comply with certain strict accounting or financial regulations, you may consider using ``max_digits=19`` and ``decimal_places=4``, see more in this `StackOverflow answer `__ It is also possible to have a nullable ``MoneyField``: .. code:: python 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 Searching for models with money fields: .. code:: python 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 The default currency code length is `3` but you can change it with the `CURRENCY_CODE_MAX_LENGTH` setting. Caution: this setting also affects the initial migration of the `exchange` plugin, so changing it after running the initial migration has no effect. (You'd need to `manage migrate exchange zero` and migrate again if you want to change it). Callable defaults ----------------- **Added in version 3.6.0: Callables were introduced in 3.6.0 and did not work before.** To make things configurable through project settings (especially useful for third-party applications using django-money), you can wrap configurable defaults in a callable. This avoids generating migrations on models when the defaults change! You can define a `default callable `__ for your MoneyField defaults, both money and separate currency. Here is an example: .. code:: python from django.conf import settings from django.db import models from djmoney.models.fields import MoneyField from djmoney.money import Money def get_default_currency(): return settings.MY_DEFAULT_CURRENCY def get_default_zero(): return Money("0.00", settings.MY_DEFAULT_CURRENCY) class Product(models.Model): # The price has no default value, but a default currency is defined! price = MoneyField(max_digits=10, decimals=2, currency_default=get_default_currency) # Discount field defaults to 0 Money and thus does not need a default currency (it's in the Money # value supplied by get_default_zero discount = MoneyField(max_digits=10, decimals=2, default=get_default_zero) Note that the supplied callables are subject to Django's internals and the fact that they need to be serializable for the migration framework. Configuring currency choices ---------------------------- It's possible to supply a global Django project setting for ``CURRENCY_CHOICES``, but you may also supply this as an individual ``currency_choices`` option for MoneyFields: .. code:: python from django.conf import settings from django.db import models from djmoney.models.fields import MoneyField class Invoice(models.Model): # Allow 2 different currencies for total amount total_amount = MoneyField(max_digits=15, decimal_places=2, currency_choices=[("CLY", "CLY"), ("USD", "USD")]) # Only allow 1 currency in the accounted field converted_amount = MoneyField(max_digits=15, decimal_places=2, currency_choices=[("CLY", "CLY")]) As with other Django Field ``choices`` kwargs, you can also supply a callable for ``currency_choices`` (but not a lambda). Please note that - similar to other ``choices`` fields - there are NO database constraints implied by this option, so you need may need additional validation safeguards and `adding your own db constraints `__. Field validation ---------------- There are 3 different possibilities for field validation: * by numeric part of money despite on currency; * by single money amount; * by multiple money amounts. All of them could be used in a combination as is shown below: .. code:: python 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}), ] ) The ``balance`` field from the model above has the following validation: * All input values should be between 10 and 1500 despite on currency; * Norwegian Crowns amount (NOK) should be between 500 and 900; * Euros should be between 100 and 1000; * US Dollars should be between 50 and 500; Constructing form data ---------------------- The default ``ModelForm`` class will use a form field (``djmoney.forms.fields.MoneyField``) that is constructed of two separate fields for amount and currency. If you need to feed data directly to such a form (for instance if you are writing a test case), then you need to pass amount and currency like this: .. code:: python # models.py class Product(models.Model): price = MoneyField( max_digits=14, decimal_places=2, default_currency='EUR' ) # forms.py class ProductForm(ModelForm): class Meta: model = Product fields = ["price"] # tests.py # construct the form in your test case form = ProductForm({'price_0': 10, 'price_1': 'EUR'}) Adding a new Currency --------------------- Currencies are listed on moneyed, and this modules use this to provide a choice list on the admin, also for validation. To add a new currency available on all the project, you can simply add these few lines to your ``settings.py`` file: .. code:: python import moneyed BOB = moneyed.add_currency( code='BOB', numeric='068', name='Peso boliviano', countries=('BOLIVIA', ) ) To restrict the currencies listed on the project set a ``CURRENCIES`` variable with a list of Currency codes on ``settings.py`` .. code:: python CURRENCIES = ('USD', 'BOB') **The list has to contain valid Currency codes** Additionally there is an ability to specify currency choices directly: .. code:: python CURRENCIES = ('USD', 'EUR') CURRENCY_CHOICES = [('USD', 'USD $'), ('EUR', 'EUR €')] Important note on model managers -------------------------------- Django-money leaves you to use any custom model managers you like for your models, but it needs to wrap some of the methods to allow searching for models with money values. This is done automatically for the "objects" attribute in any model that uses MoneyField. However, if you assign managers to some other attribute, you have to wrap your manager manually, like so: .. code:: python 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()) Also, the money\_manager wrapper only wraps the standard QuerySet methods. If you define custom QuerySet methods, that do not end up using any of the standard ones (like "get", "filter" and so on), then you also need to manually decorate those custom methods, like so: .. code:: python from djmoney.models.managers import understands_money class MyCustomQuerySet(QuerySet): @understands_money def my_custom_method(*args, **kwargs): # Awesome stuff Note on serialization --------------------- Django-money provides a custom deserializer, it is not registered by default so you will have to actively register it in your ``settings.py``. .. code:: python SERIALIZATION_MODULES = {"json": "djmoney.serializers"} Format localization ------------------- The formatting is turned on if you have set ``USE_L10N = True`` in the your settings file. If formatting is disabled in the configuration, then in the templates will be used default formatting. In the templates you can use a special tag to format the money. In the file ``settings.py`` add to ``INSTALLED_APPS`` entry from the library ``djmoney``: .. code:: python INSTALLED_APPS += ('djmoney', ) In the template, add: :: {% load djmoney %} ... {% money_localize money %} and that is all. Instructions to the tag ``money_localize``: :: {% money_localize [ on(default) | off ] [as var_name] %} {% money_localize [ on(default) | off ] [as var_name] %} Examples: The same effect: :: {% money_localize money_object %} {% money_localize money_object on %} Assignment to a variable: :: {% money_localize money_object on as NEW_MONEY_OBJECT %} Formatting the number with currency: :: {% money_localize '4.5' 'USD' %} :: Return:: Money object Testing ------- Install the required packages: :: git clone https://github.com/django-money/django-money cd ./django-money/ pip install -e ".[test]" # installation with required packages for testing Recommended way to run the tests: .. code:: bash tox Testing the application in the current environment python: .. code:: bash make test Working with Exchange Rates --------------------------- To work with exchange rates, add the following to your ``INSTALLED_APPS``. .. code:: python INSTALLED_APPS = [ ..., 'djmoney.contrib.exchange', ] Also, it is required to have ``certifi`` installed. It could be done via installing ``djmoney`` with ``exchange`` extra: .. code:: bash pip install "django-money[exchange]" To create required relations run ``python manage.py migrate``. To fill these relations with data you need to choose a data source. Currently, 2 data sources are supported - https://openexchangerates.org/ (default) and https://fixer.io/. To choose another data source set ``EXCHANGE_BACKEND`` settings with importable string to the backend you need: .. code:: python EXCHANGE_BACKEND = 'djmoney.contrib.exchange.backends.FixerBackend' If you want to implement your own backend, you need to extend ``djmoney.contrib.exchange.backends.base.BaseExchangeBackend``. Two data sources mentioned above are not open, so you have to specify access keys in order to use them: ``OPEN_EXCHANGE_RATES_APP_ID`` - '' ``FIXER_ACCESS_KEY`` - '' Backends return rates for a base currency, by default it is USD, but could be changed via ``BASE_CURRENCY`` setting. Open Exchanger Rates & Fixer supports some extra stuff, like historical data or restricting currencies in responses to the certain list. In order to use these features you could change default URLs for these backends: .. code:: python 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' Or, you could pass it directly to ``update_rates`` method: .. code:: python >>> 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') There is a possibility to use multiple backends in the same time: .. code:: python >>> 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) Regular operations with ``Money`` will use ``EXCHANGE_BACKEND`` backend to get the rates. Also, there are two management commands for updating rates and removing them: .. code:: bash $ python manage.py update_rates Successfully updated rates from openexchangerates.org $ python manage.py clear_rates Successfully cleared rates for openexchangerates.org Both of them accept ``-b/--backend`` option, that will update/clear data only for this backend. And ``clear_rates`` accepts ``-a/--all`` option, that will clear data for all backends. To set up a periodic rates update you could use Celery task: .. code:: python CELERY_BEAT_SCHEDULE = { 'update_rates': { 'task': 'path.to.your.task', 'schedule': crontab(minute=0, hour=0), 'kwargs': {} # For custom arguments } } Example task implementation: .. code:: python 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) To convert one currency to another: .. code:: python >>> from djmoney.money import Money >>> from djmoney.contrib.exchange.models import convert_money >>> convert_money(Money(100, 'EUR'), 'USD') Exchange rates are integrated with Django Admin. django-money can be configured to automatically use this app for currency conversions by settings ``AUTO_CONVERT_MONEY = True`` in your Django settings. Note that currency conversion is a lossy process, so automatic conversion is usually a good strategy only for very simple use cases. For most use cases you will need to be clear about exactly when currency conversion occurs, and automatic conversion can hide bugs. Also, with automatic conversion you lose some properties like commutativity (``A + B == B + A``) due to conversions happening in different directions. Usage with Django REST Framework -------------------------------- Make sure that ``djmoney`` and is in the ``INSTALLED_APPS`` of your ``settings.py`` and that ``rest_framework`` has been installed. MoneyField will automatically register a serializer for Django REST Framework through ``djmoney.apps.MoneyConfig.ready()``. You can add a serializable field the following way: .. code:: python from djmoney.contrib.django_rest_framework import MoneyField class Serializers(serializers.Serializer): my_computed_prop = MoneyField(max_digits=10, decimal_places=2) Built-in serializer works in the following way: .. code:: python 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'), ]) Note that when specifying individual fields on your serializer, the amount and currency fields are treated separately. To achieve the same behaviour as above you would include both field names: .. code:: python class Serializer(serializers.ModelSerializer): class Meta: model = Expenses fields = ('id', 'amount', 'amount_currency') Customization ------------- If there is a need to customize the process deconstructing ``Money`` instances onto Django Fields and the other way around, then it is possible to use a custom descriptor like this one: .. code:: python class MyMoneyDescriptor: def __get__(self, obj, type=None): amount = obj.__dict__[self.field.name] return Money(amount, "EUR") It will always use ``EUR`` for all ``Money`` instances when ``obj.money`` is called. Then it should be passed to ``MoneyField``: .. code:: python class Expenses(models.Model): amount = MoneyField(max_digits=10, decimal_places=2, money_descriptor_class=MyMoneyDescriptor) Background ---------- This project is a fork of the Django support that was in http://code.google.com/p/python-money/ This version adds tests, and comes with several critical bugfixes. django-money/djmoney/000077500000000000000000000000001521124646200151155ustar00rootroot00000000000000django-money/djmoney/__init__.py000066400000000000000000000000261521124646200172240ustar00rootroot00000000000000__version__ = "3.6.1" django-money/djmoney/_compat.py000066400000000000000000000010641521124646200171120ustar00rootroot00000000000000# flake8: noqa """ This module isolates code that has to do with compatibility issues between different supported versions of Python or other dependencies. This is quite important to keep the codebase clean. Please do not catch ImportError exceptions other places than here :) """ def setup_managers(sender): from .models.managers import money_manager default_manager_name = sender._meta.default_manager_name or "objects" for manager in filter(lambda m: m.name == default_manager_name, sender._meta.local_managers): money_manager(manager) django-money/djmoney/admin.py000066400000000000000000000012301521124646200165530ustar00rootroot00000000000000import django.contrib.admin.helpers as admin_helpers import django.contrib.admin.templatetags.admin_list as admin_list import django.contrib.admin.utils as admin_utils from .models.fields import MoneyField MODULES_TO_PATCH = [admin_utils, admin_helpers, admin_list] def setup_admin_integration(): original_display_for_field = admin_utils.display_for_field def display_for_field(value, field, *args, **kwargs): if isinstance(field, MoneyField): return str(value) return original_display_for_field(value, field, *args, **kwargs) for mod in MODULES_TO_PATCH: setattr(mod, "display_for_field", display_for_field) django-money/djmoney/apps.py000066400000000000000000000006521521124646200164350ustar00rootroot00000000000000from django.apps import AppConfig class MoneyConfig(AppConfig): name = "djmoney" default_auto_field = "django.db.models.AutoField" def ready(self): try: from .contrib.django_rest_framework import register_money_field register_money_field() except ImportError: pass from djmoney.admin import setup_admin_integration setup_admin_integration() django-money/djmoney/contrib/000077500000000000000000000000001521124646200165555ustar00rootroot00000000000000django-money/djmoney/contrib/__init__.py000066400000000000000000000000001521124646200206540ustar00rootroot00000000000000django-money/djmoney/contrib/django_rest_framework/000077500000000000000000000000001521124646200231315ustar00rootroot00000000000000django-money/djmoney/contrib/django_rest_framework/__init__.py000066400000000000000000000000751521124646200252440ustar00rootroot00000000000000from .fields import MoneyField, register_money_field # noqa django-money/djmoney/contrib/django_rest_framework/fields.py000066400000000000000000000071541521124646200247600ustar00rootroot00000000000000from django.core.exceptions import FieldDoesNotExist from django.core.validators import MaxValueValidator, MinValueValidator from django.utils.translation import gettext_lazy as _ from rest_framework.fields import empty from rest_framework.serializers import DecimalField, ModelSerializer from djmoney.models.fields import MoneyField as ModelField from djmoney.models.validators import MaxMoneyValidator, MinMoneyValidator from djmoney.money import Money from djmoney.utils import MONEY_CLASSES, get_currency_field_name from moneyed.classes import CurrencyDoesNotExist class _PrimitiveMoney: """ A container for ``Money`` data that does not do any validation of said data. It conveniently holds the amount and currency attributes to ease transformation into a valid ``Money`` instance. """ __slots__ = ("amount", "currency") def __init__(self, amount, currency): self.amount = amount self.currency = currency class MoneyField(DecimalField): """ Treats ``Money`` objects as decimal values in representation and does decimal's validation during transformation to native value. """ default_error_messages = { "invalid_currency": _("{currency!r} is not a valid currency"), } def __init__(self, *args, **kwargs): self.default_currency = kwargs.pop("default_currency", None) super().__init__(*args, **kwargs) # Rest Framework converts `min_value` / `max_value` to validators, that are not aware about `Money` class # We need to adjust them for idx, validator in enumerate(self.validators): if isinstance(validator, MinValueValidator): self.validators[idx] = MinMoneyValidator(self.min_value) elif isinstance(validator, MaxValueValidator): self.validators[idx] = MaxMoneyValidator(self.max_value) def to_representation(self, obj): """ When ``field_currency`` is not in ``self.validated_data`` then ``obj`` is an instance of ``Decimal``, otherwise it is ``Money``. """ if isinstance(obj, MONEY_CLASSES): obj = obj.amount return super().to_representation(obj) def to_internal_value(self, data): if isinstance(data, MONEY_CLASSES + (_PrimitiveMoney,)): amount = super().to_internal_value(data.amount) try: return Money(amount, data.currency) except CurrencyDoesNotExist: self.fail("invalid_currency", currency=data.currency) return super().to_internal_value(data) def get_value(self, data): parent_meta = getattr(self.parent, "Meta", None) default_currency = self.default_currency if parent_meta and hasattr(parent_meta, "model"): model = self.parent.Meta.model try: field = model._meta.get_field(self.source) default_currency = field.default_currency except FieldDoesNotExist as e: if not hasattr(model, self.source): raise ValueError( f"{self.source} is neither a db field nor a property on the model {model.__name__}" ) from e amount = super().get_value(data) currency = data.get(get_currency_field_name(self.field_name), self.default_currency or default_currency) if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty: return _PrimitiveMoney(amount=amount, currency=currency) return amount def register_money_field(): ModelSerializer.serializer_field_mapping[ModelField] = MoneyField django-money/djmoney/contrib/exchange/000077500000000000000000000000001521124646200203375ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/__init__.py000066400000000000000000000000001521124646200224360ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/admin.py000066400000000000000000000005741521124646200220070ustar00rootroot00000000000000from django.contrib import admin from .models import Rate class RateAdmin(admin.ModelAdmin): list_display = ("currency", "value", "last_update", "backend") list_filter = ("currency",) ordering = ("currency",) readonly_fields = ("backend",) def last_update(self, instance): return instance.backend.last_update admin.site.register(Rate, RateAdmin) django-money/djmoney/contrib/exchange/backends/000077500000000000000000000000001521124646200221115ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/backends/__init__.py000066400000000000000000000002271521124646200242230ustar00rootroot00000000000000from .fixer import FixerBackend # noqa from .openexchangerates import OpenExchangeRatesBackend # noqa from .unirateapi import UniRateBackend # noqa django-money/djmoney/contrib/exchange/backends/base.py000066400000000000000000000050701521124646200233770ustar00rootroot00000000000000import json import ssl from decimal import Decimal from typing import Optional from urllib.parse import parse_qsl, urlparse, urlunparse from urllib.request import Request, urlopen from django.db.transaction import atomic from django.utils.http import urlencode from djmoney import settings from ..models import ExchangeBackend, Rate try: import certifi except ImportError: raise ImportError("Please install dependency certifi - pip install certifi") class BaseExchangeBackend: name: Optional[str] = None url: Optional[str] = None def get_rates(self, **kwargs): """ Returns a mapping : . """ raise NotImplementedError def get_url(self, **params): """ Updates base url with provided GET parameters. """ parts = list(urlparse(self.url)) query = dict(parse_qsl(parts[4])) query.update(params) parts[4] = urlencode(query) return urlunparse(parts) def get_params(self): """ Default GET parameters for the request. """ return {} def get_response(self, **params): headers = { "User-Agent": "Mozilla/5.0", } request = Request(self.get_url(**params), headers=headers) context = ssl.create_default_context(cafile=certifi.where()) response = urlopen(request, context=context) return response.read() def parse_json(self, response): if isinstance(response, bytes): response = response.decode("utf-8") return json.loads(response, parse_float=Decimal) @atomic def update_rates(self, base_currency=settings.BASE_CURRENCY, **kwargs): """ Updates rates for the given backend. """ backend, _ = ExchangeBackend.objects.update_or_create(name=self.name, defaults={"base_currency": base_currency}) params = self.get_params() params.update(base_currency=base_currency, **kwargs) Rate.objects.bulk_create( [ Rate(currency=currency, value=value, backend=backend) for currency, value in self.get_rates(**params).items() ], update_conflicts=True, unique_fields=["currency", "backend"], update_fields=["value"], ) class SimpleExchangeBackend(BaseExchangeBackend): """ Simple backend implementation. Assumes JSON response with `rates` key inside. """ def get_rates(self, **params): response = self.get_response(**params) return self.parse_json(response)["rates"] django-money/djmoney/contrib/exchange/backends/fixer.py000066400000000000000000000012541521124646200236020ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from djmoney import settings from .base import SimpleExchangeBackend class FixerBackend(SimpleExchangeBackend): name = "fixer.io" def __init__(self, url=settings.FIXER_URL, access_key=settings.FIXER_ACCESS_KEY): if access_key is None: raise ImproperlyConfigured("settings.FIXER_ACCESS_KEY should be set to use FixerBackend") self.url = url self.access_key = access_key def get_params(self): # support both `data.fixer.io` and `api.apilayer.com` auth params return { "apikey": self.access_key, "access_key": self.access_key, } django-money/djmoney/contrib/exchange/backends/openexchangerates.py000066400000000000000000000013201521124646200261620ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from djmoney import settings from .base import SimpleExchangeBackend class OpenExchangeRatesBackend(SimpleExchangeBackend): name = "openexchangerates.org" url = settings.OPEN_EXCHANGE_RATES_URL def __init__(self, url=settings.OPEN_EXCHANGE_RATES_URL, access_key=settings.OPEN_EXCHANGE_RATES_APP_ID): if access_key is None: raise ImproperlyConfigured( "settings.OPEN_EXCHANGE_RATES_APP_ID should be set to use OpenExchangeRatesBackend" ) self.url = url self.access_key = access_key def get_params(self): return {"app_id": self.access_key, "base": settings.BASE_CURRENCY} django-money/djmoney/contrib/exchange/backends/unirateapi.py000066400000000000000000000011211521124646200246170ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from djmoney import settings from .base import SimpleExchangeBackend class UniRateBackend(SimpleExchangeBackend): name = "unirateapi.com" def __init__(self, url=settings.UNIRATE_URL, access_key=settings.UNIRATE_ACCESS_KEY): if access_key is None: raise ImproperlyConfigured("settings.UNIRATE_ACCESS_KEY should be set to use UniRateBackend") self.url = url self.access_key = access_key def get_params(self): return {"api_key": self.access_key, "from": settings.BASE_CURRENCY} django-money/djmoney/contrib/exchange/exceptions.py000066400000000000000000000001211521124646200230640ustar00rootroot00000000000000class MissingRate(Exception): """ Absence of some exchange rate. """ django-money/djmoney/contrib/exchange/management/000077500000000000000000000000001521124646200224535ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/management/__init__.py000066400000000000000000000000001521124646200245520ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/management/base.py000066400000000000000000000011761521124646200237440ustar00rootroot00000000000000from django.core.management.base import BaseCommand from djmoney import settings class BaseExchangeCommand(BaseCommand): """ Basic command for exchange rates manipulation. Provides ``backend`` argument. """ def add_arguments(self, parser): parser.add_argument( "-b", "--backend", action="store", dest="backend", help="Importable string for custom exchange rates backend.", required=False, default=settings.EXCHANGE_BACKEND, ) def success(self, message): self.stdout.write(self.style.SUCCESS(message)) django-money/djmoney/contrib/exchange/management/commands/000077500000000000000000000000001521124646200242545ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/management/commands/__init__.py000066400000000000000000000000001521124646200263530ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/management/commands/clear_rates.py000066400000000000000000000016231521124646200271140ustar00rootroot00000000000000from django.utils.module_loading import import_string from djmoney.contrib.exchange.models import Rate from ..base import BaseExchangeCommand class Command(BaseExchangeCommand): help = "Clears exchange rates." def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( "--all", action="store_true", dest="all", help="Clear rates for all backends.", required=False, default=False, ) def handle(self, *args, **options): if options["all"]: Rate.objects.all().delete() message = "Successfully cleared all rates" else: backend = import_string(options["backend"]) Rate.objects.filter(backend=backend.name).delete() message = "Successfully cleared rates for %s" % backend.name self.success(message) django-money/djmoney/contrib/exchange/management/commands/update_rates.py000066400000000000000000000005611521124646200273100ustar00rootroot00000000000000from django.utils.module_loading import import_string from ..base import BaseExchangeCommand class Command(BaseExchangeCommand): help = "Updates exchange rates." def handle(self, *args, **options): backend = import_string(options["backend"])() backend.update_rates() self.success("Successfully updated rates from %s" % backend.name) django-money/djmoney/contrib/exchange/migrations/000077500000000000000000000000001521124646200225135ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/migrations/0001_initial.py000066400000000000000000000025561521124646200251660ustar00rootroot00000000000000# Generated by Django 2.0.4 on 2018-04-03 08:42 import django.db.models.deletion from django.db import migrations, models from djmoney.settings import CURRENCY_CODE_MAX_LENGTH class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="ExchangeBackend", fields=[ ("name", models.CharField(max_length=255, primary_key=True, serialize=False)), ("last_update", models.DateTimeField(auto_now=True)), ("base_currency", models.CharField(max_length=CURRENCY_CODE_MAX_LENGTH)), ], ), migrations.CreateModel( name="Rate", fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("currency", models.CharField(max_length=CURRENCY_CODE_MAX_LENGTH)), ("value", models.DecimalField(decimal_places=6, max_digits=20)), ( "backend", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="exchange.ExchangeBackend", related_name="rates" ), ), ], ), migrations.AlterUniqueTogether(name="rate", unique_together={("currency", "backend")}), ] django-money/djmoney/contrib/exchange/migrations/__init__.py000066400000000000000000000000001521124646200246120ustar00rootroot00000000000000django-money/djmoney/contrib/exchange/models.py000066400000000000000000000076341521124646200222060ustar00rootroot00000000000000from django.conf import settings from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.module_loading import import_string from djmoney.settings import CURRENCY_CODE_MAX_LENGTH, EXCHANGE_BACKEND, RATES_CACHE_TIMEOUT from .exceptions import MissingRate class ExchangeBackend(models.Model): name = models.CharField(max_length=255, primary_key=True) last_update = models.DateTimeField(auto_now=True) base_currency = models.CharField(max_length=CURRENCY_CODE_MAX_LENGTH) def __str__(self): return self.name def clear_rates(self): self.rates.all().delete() class Rate(models.Model): id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID") currency = models.CharField(max_length=CURRENCY_CODE_MAX_LENGTH) value = models.DecimalField(max_digits=20, decimal_places=6) backend = models.ForeignKey(ExchangeBackend, on_delete=models.CASCADE, related_name="rates") class Meta: unique_together = (("currency", "backend"),) def get_default_backend_name(): return import_string(EXCHANGE_BACKEND).name def get_rate(source, target, backend=None): """ Returns an exchange rate between source and target currencies. Converts exchange rate on the DB side if there is no backends with given base currency. Uses data from the default backend if the backend is not specified. """ if str(source) == str(target): return 1 if backend is None: backend = get_default_backend_name() key = f"djmoney:get_rate:{source}:{target}:{backend}" result = cache.get(key) if result is not None: return result result = _get_rate(source, target, backend) cache.set(key, result, RATES_CACHE_TIMEOUT) return result def _get_rate(source, target, backend): source, target = str(source), str(target) rates = Rate.objects.filter(currency__in=(source, target), backend=backend).select_related("backend") if not rates: raise MissingRate(f"Rate {source} -> {target} does not exist") if len(rates) == 1: return _try_to_get_rate_directly(source, target, rates[0]) return _get_rate_via_base(rates, target) def _try_to_get_rate_directly(source, target, rate): """ Either target or source equals to base currency of existing rate. """ # Converting from base currency to target if rate.backend.base_currency == source and rate.currency == target: return rate.value # Converting from target currency to base elif rate.backend.base_currency == target and rate.currency == source: return 1 / rate.value # Case when target or source is not a base currency raise MissingRate(f"Rate {source} -> {target} does not exist") def _get_rate_via_base(rates, target): """ :param: rates: A set/tuple of two base Rate instances :param: target: A string instance of the currency to convert to Both target and source are not a base currency - actual rate could be calculated via their rates to base currency. For example: 7.84 NOK = 1 USD = 8.37 SEK 7.84 NOK = 8.37 SEK 1 NOK = 8.37 / 7.84 SEK """ first, second = rates # Instead of expecting an explicit order in the `rates` iterable, that will put the # source currency in the first place, we decided to add an extra check here and swap # items if they are ordered not as expected if first.currency == target: first, second = second, first return second.value / first.value def convert_money(value, currency, backend=None): if "djmoney.contrib.exchange" not in settings.INSTALLED_APPS: raise ImproperlyConfigured( "You have to add 'djmoney.contrib.exchange' to INSTALLED_APPS in order to use currency exchange" ) amount = value.amount * get_rate(value.currency, currency, backend=backend) return value.__class__(amount, currency) django-money/djmoney/forms/000077500000000000000000000000001521124646200162435ustar00rootroot00000000000000django-money/djmoney/forms/__init__.py000066400000000000000000000000751521124646200203560ustar00rootroot00000000000000from .fields import * # noqa from .widgets import * # noqa django-money/djmoney/forms/fields.py000066400000000000000000000112231521124646200200620ustar00rootroot00000000000000from django.core.exceptions import ValidationError from django.forms import ChoiceField, DecimalField, MultiValueField from djmoney.money import Money from djmoney.utils import MONEY_CLASSES from ..settings import CURRENCY_CHOICES, DECIMAL_PLACES from .widgets import HiddenMoneyWidget, MoneyWidget __all__ = ("MoneyField",) class MoneyField(MultiValueField): def __init__( self, currency_widget=None, currency_choices=CURRENCY_CHOICES, max_value=None, min_value=None, max_digits=None, decimal_places=DECIMAL_PLACES, default_amount=None, default_currency=None, *args, **kwargs, ): amount_field = DecimalField( *args, max_value=max_value, min_value=min_value, max_digits=max_digits, decimal_places=decimal_places, **kwargs, ) currency_field = ChoiceField(choices=currency_choices) self.widget = MoneyWidget( amount_widget=amount_field.widget, currency_widget=currency_widget or currency_field.widget, default_currency=default_currency, ) # The two fields that this widget comprises fields = (amount_field, currency_field) super().__init__(fields, *args, **kwargs) # set the initial value to the default currency so that the # default currency appears as the selected menu item # Callables might be supplied in cases where the initial data is directly copied from the MoneyField db # object - this seems to happen internally when Django auto-generates certain hidden input fields to track # initial data in a formset. if callable(default_currency): default_currency = default_currency() # TODO: are we sure we do not have default_amount callables here? if isinstance(default_amount, Money): default_amount = default_amount.amount self.initial = [default_amount, default_currency] def hidden_widget(self): # TODO: This should inherit the constraints of the field # Otherwise, we won't validate that min, max value and currency_choices are valid? # This is usually used to pre-fill the 'initial data' hidden input, so it's not very sensitive to tampering. return HiddenMoneyWidget() def compress(self, data_list): if data_list: if not self.required and data_list[0] in self.empty_values: return None else: return Money(*data_list[:2]) return None def clean(self, value): if isinstance(value, MONEY_CLASSES): value = (value.amount, value.currency) # Unpack the list and fail if it doesn't match the expected structure if isinstance(value, (list, tuple)): amount, currency = value value = (amount, currency) return super().clean(value) def has_changed(self, initial, data): # noqa if self.disabled: return False if initial is None: initial = ["" for _ in range(0, len(data))] else: # If the initial value was supplied as a list, go with that. # Otherwise, try to decompress it. if not isinstance(initial, list): initial = self.widget.decompress(initial) amount_field, currency_field = self.fields amount_initial, currency_initial = initial # We treat the amount and currency fields slightly # differently: if the amount has changed, then we definitely # consider the money value to have changed. If the currency # has changed, but the amount is *empty* then we do not # consider the money value to have changed. This means that it # plays nicely with empty form rows in formsets. try: amount_data = data[0] except IndexError: amount_data = None try: amount_initial = amount_field.to_python(amount_initial) except ValidationError: return True if amount_field.has_changed(amount_initial, amount_data): return True try: currency_data = data[1] except IndexError: currency_data = None try: currency_initial = currency_field.to_python(currency_initial) except ValidationError: return True # If the currency is valid, has changed and there is some # amount data, then the money value has changed. if currency_field.has_changed(currency_initial, currency_data) and amount_data: return True return False django-money/djmoney/forms/widgets.py000066400000000000000000000034601521124646200202660ustar00rootroot00000000000000from django.forms import HiddenInput, MultiWidget, Select, TextInput from ..settings import CURRENCY_CHOICES __all__ = ( "HiddenMoneyWidget", "MoneyWidget", ) class HiddenMoneyWidget(HiddenInput): def format_value(self, value): if value is None: return "" # For hidden inputs, we are sometimes supplied with a pre-formatted value if isinstance(value, str): return value # The hidden input widget is used in a MultiField (MoneyField!) if isinstance(value, list): amount, currency = value return f"{amount} {currency}" # Assuming it's a money object return f"{value.amount} {value.currency}" class MoneyWidget(MultiWidget): def __init__( self, choices=CURRENCY_CHOICES, amount_widget=TextInput, currency_widget=None, default_currency=None, *args, **kwargs, ): self.default_currency = default_currency if not currency_widget: currency_widget = Select(choices=choices) widgets = (amount_widget, currency_widget) super().__init__(widgets, *args, **kwargs) def decompress(self, value): if value is not None: # Value provided by hidden input submissions somehow being evaluated in this widget # TBD: Why does that happen when MoneyField.hidden_widget() is defined? if isinstance(value, str): if " " in value: amount, currency = value.split(" ") return [amount, currency] raise ValueError(f"Invalid money value: {value}") if isinstance(value, (list, tuple)): return value return [value.amount, value.currency] return [None, self.default_currency] django-money/djmoney/models/000077500000000000000000000000001521124646200164005ustar00rootroot00000000000000django-money/djmoney/models/__init__.py000066400000000000000000000000001521124646200204770ustar00rootroot00000000000000django-money/djmoney/models/fields.py000066400000000000000000000341171521124646200202260ustar00rootroot00000000000000from decimal import Decimal from django.core.exceptions import ValidationError from django.core.validators import DecimalValidator from django.db import models from django.db.models import NOT_PROVIDED, F, Field, Func, Value from django.db.models.expressions import BaseExpression from django.db.models.signals import class_prepared from django.utils.encoding import smart_str from django.utils.functional import cached_property from djmoney import forms from djmoney.money import Currency, Money from moneyed import Money as OldMoney from .._compat import setup_managers from ..settings import CURRENCY_CHOICES, CURRENCY_CODE_MAX_LENGTH, DECIMAL_PLACES, DEFAULT_CURRENCY from ..utils import MONEY_CLASSES, get_currency_field_name, prepare_expression __all__ = ("MoneyField",) class MoneyValidator(DecimalValidator): def __call__(self, value): return super().__call__(value.amount) def get_value(obj, expr): """ Extracts value from object or expression. """ if isinstance(expr, F): expr = getattr(obj, expr.name) else: expr = expr.value if isinstance(expr, OldMoney): expr = Money(expr.amount, expr.currency) return expr def validate_money_expression(obj, expr): """ Money supports different types of expressions, but you can't do following: - Add or subtract money with not-money - Any exponentiation - Any operations with money in different currencies - Multiplication, division, modulo with money instances on both sides of expression """ connector = expr.connector lhs = get_value(obj, expr.lhs) rhs = get_value(obj, expr.rhs) if (not isinstance(rhs, Money) and connector in ("+", "-")) or connector == "^": raise ValidationError("Invalid F expression for MoneyField.", code="invalid") if isinstance(lhs, Money) and isinstance(rhs, Money): if connector in ("*", "/", "^", "%%"): raise ValidationError("Invalid F expression for MoneyField.", code="invalid") if lhs.currency != rhs.currency: raise ValidationError("You cannot use F() with different currencies.", code="invalid") def validate_money_value(value): """ Valid value for money are: - Single numeric value - Money instances - Pairs of numeric value and currency. Currency can't be None. """ if isinstance(value, (list, tuple)) and (len(value) != 2 or value[1] is None): raise ValidationError("Invalid value for MoneyField: %(value)s.", code="invalid", params={"value": value}) def get_currency(value): """ Extracts currency from value. """ if isinstance(value, MONEY_CLASSES): return smart_str(value.currency) elif isinstance(value, (list, tuple)): return value[1] class MoneyFieldProxy: def __init__(self, field): self.field = field self.currency_field_name = get_currency_field_name(self.field.name, self.field) def _money_from_obj(self, obj): amount = obj.__dict__[self.field.name] currency = obj.__dict__[self.currency_field_name] if amount is None: return None elif currency is None: raise TypeError("Currency code can't be None") return Money(amount=amount, currency=currency, decimal_places=self.field.decimal_places) def __get__(self, obj, type=None): if obj is None: return self data = obj.__dict__ if isinstance(data[self.field.name], BaseExpression): return data[self.field.name] if not isinstance(data[self.field.name], Money): data[self.field.name] = self._money_from_obj(obj) return data[self.field.name] def __set__(self, obj, value): # noqa if ( value is not None and self.field._currency_field.null and not isinstance(value, MONEY_CLASSES) and not obj.__dict__[self.currency_field_name] ): # For nullable fields we need either both NULL amount and currency or both NOT NULL raise ValueError("Missing currency value") if isinstance(value, BaseExpression): if isinstance(value, Value): value = self.prepare_value(obj, value.value) elif not isinstance(value, Func): validate_money_expression(obj, value) prepare_expression(value) else: value = self.prepare_value(obj, value) obj.__dict__[self.field.name] = value def prepare_value(self, obj, value): validate_money_value(value) currency = get_currency(value) if currency: self.set_currency(obj, currency) return self.field.to_python(value) def set_currency(self, obj, value): # we have to determine whether to replace the currency. # i.e. if we do the following: # .objects.get_or_create(money_currency='EUR') # then the currency is already set up, before this code hits # __set__ of MoneyField. This is because the currency field # has less creation counter than money field. # # Gotcha: # But we should also allow setting a field back to its original default # value! # https://github.com/django-money/django-money/issues/221 object_currency = obj.__dict__.get(self.currency_field_name) if object_currency != value: # in other words, update the currency only if it wasn't # changed before. setattr(obj, self.currency_field_name, value) class CurrencyField(models.CharField): description = "A field which stores currency." def __init__(self, price_field=None, default=DEFAULT_CURRENCY, **kwargs): if isinstance(default, Currency): default = default.code kwargs.setdefault("max_length", CURRENCY_CODE_MAX_LENGTH) self.price_field = price_field super().__init__(default=default, **kwargs) def contribute_to_class(self, cls, name): if name not in [f.name for f in cls._meta.fields]: super().contribute_to_class(cls, name) class MoneyField(models.DecimalField): description = "A field which stores both the currency and amount of money." @property def non_db_attrs(self): return super().non_db_attrs + ("default_currency", "currency_choices") def __init__( self, verbose_name=None, name=None, max_digits=None, decimal_places=DECIMAL_PLACES, default=NOT_PROVIDED, default_currency=DEFAULT_CURRENCY, currency_choices=CURRENCY_CHOICES, currency_max_length=CURRENCY_CODE_MAX_LENGTH, currency_field_name=None, money_descriptor_class=MoneyFieldProxy, **kwargs, ): nullable = kwargs.get("null", False) default = self.setup_default(default, default_currency, nullable) # Provide the default currency from the default Money instance provided if not default_currency and isinstance(default, Money): default_currency = default.currency self.currency_max_length = currency_max_length self.default_currency = default_currency self.currency_choices = currency_choices self.currency_field_name = currency_field_name self.money_descriptor_class = money_descriptor_class super().__init__(verbose_name, name, max_digits, decimal_places, default=default, **kwargs) self.creation_counter += 1 Field.creation_counter += 1 def setup_default(self, default, default_currency, nullable): # None and NOT_PROVIDED are returned as-is if default in (None, NOT_PROVIDED): return default # Money instances need no further processing elif isinstance(default, Money): return default # Callables continue to be callables (will be evaluated later) # Evaluating them here will create new migrations every time the callable returns a new value elif callable(default): return default # This is not supported: If default_currency is a callable, we need to fail, we cannot call it to construct the # default, that would have unintended consequences. Require to have BOTH default and default_currency as # callables in that case. elif callable(default_currency) and default_currency != NOT_PROVIDED: raise ValueError( "django-money does not support a callable for default_currency, if the amount's default isn't also a " "callable." ) elif isinstance(default, (str, bytes)): try: # handle scenario where default is formatted like: # 'amount currency-code' amount, currency = (default.decode() if isinstance(default, bytes) else default).split(" ", 1) except ValueError: # value error would be risen if the default is # without the currency part, i.e # 'amount' amount = default.decode() if isinstance(default, bytes) else default currency = default_currency amount = Decimal(amount) elif isinstance(default, (float, Decimal, int)): amount, currency = default, default_currency elif isinstance(default, OldMoney): amount, currency = default.amount, default.currency else: raise ValueError(f"default value must be an instance of Money, is: {default}") assert currency is not None, ( "Default currency can not be `None` when default is of a value that doesn't include a currency. Either" " provide a default containing a currency value or configure a currency default via setting" " `default_currency=` or `DEFAULT_CURRENCY`(global)" ) return Money(amount, currency) def to_python(self, value): if isinstance(value, MONEY_CLASSES): value = value.amount elif isinstance(value, tuple): value = value[0] if isinstance(value, float): value = str(value) return super().to_python(value) def clean(self, value, model_instance): """ We need to run validation against ``Money`` instance. """ output = self.to_python(value) self.validate(value, model_instance) self.run_validators(value) return output @cached_property def validators(self): """ Default ``DecimalValidator`` doesn't work with ``Money`` instances. """ return super(models.DecimalField, self).validators + [MoneyValidator(self.max_digits, self.decimal_places)] def contribute_to_class(self, cls, name): cls._meta.has_money_field = True # Note the discussion about whether or not the currency field should be added in migrations: # https://github.com/django-money/django-money/issues/725 # https://github.com/django-money/django-money/pull/726 # https://github.com/django-money/django-money/issues/731 if not hasattr(self, "_currency_field"): self.add_currency_field(cls, name) super().contribute_to_class(cls, name) setattr(cls, self.name, self.money_descriptor_class(self)) def add_currency_field(self, cls, name): """ Adds CurrencyField instance to a model class. """ currency_field = CurrencyField( price_field=self, max_length=self.currency_max_length, default=self.default_currency, editable=False, choices=self.currency_choices, null=self.null, ) currency_field.creation_counter = self.creation_counter - 1 currency_field_name = get_currency_field_name(name, self) cls.add_to_class(currency_field_name, currency_field) self._currency_field = currency_field def get_db_prep_save(self, value, connection): if isinstance(value, MONEY_CLASSES): value = value.amount return super().get_db_prep_save(value, connection) def get_default(self): if isinstance(self.default, Money): return self.default else: return super().get_default() @property def _has_default(self): # Whether the field has an explicitly provided non-empty default. # `None` was used by django-money before, and we need to check it because it can come from historical migrations return self.default not in (None, NOT_PROVIDED) def formfield(self, **kwargs): defaults = {"form_class": forms.MoneyField, "decimal_places": self.decimal_places} defaults.update(kwargs) defaults["currency_choices"] = self.currency_choices defaults["default_currency"] = self.default_currency if self._has_default: if callable(self.default): defaults["default_amount"] = self.default().amount else: defaults["default_amount"] = self.default.amount return super().formfield(**defaults) def value_to_string(self, obj): value = self.value_from_object(obj) return self.get_prep_value(value) def deconstruct(self): name, path, args, kwargs = super().deconstruct() # Comment needed: Why is this done? if self.default_currency != DEFAULT_CURRENCY: kwargs["default_currency"] = self.default_currency # Comment needed: Why is this done? if self.currency_choices != CURRENCY_CHOICES: kwargs["currency_choices"] = self.currency_choices if self.currency_field_name: kwargs["currency_field_name"] = self.currency_field_name if self.currency_max_length != CURRENCY_CODE_MAX_LENGTH: kwargs["currency_max_length"] = self.currency_max_length return name, path, args, kwargs def patch_managers(sender, **kwargs): """ Patches models managers. """ if sender._meta.proxy_for_model: has_money_field = hasattr(sender._meta.proxy_for_model._meta, "has_money_field") else: has_money_field = hasattr(sender._meta, "has_money_field") if has_money_field: setup_managers(sender) class_prepared.connect(patch_managers) django-money/djmoney/models/managers.py000066400000000000000000000222371521124646200205550ustar00rootroot00000000000000from functools import wraps from django.core.exceptions import FieldDoesNotExist from django.db.models import NOT_PROVIDED, Case, F, Q from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import BaseExpression from django.db.models.functions import Cast, Coalesce from django.utils.encoding import smart_str from ..utils import MONEY_CLASSES, get_currency_field_name, prepare_expression from .fields import CurrencyField, MoneyField def _get_clean_name(model, name): # Get rid of __lt, __gt etc for the currency lookup if LOOKUP_SEP not in name: return name lookup_fields = name.split(LOOKUP_SEP) field = _get_field(model, name) return name.rsplit(LOOKUP_SEP, lookup_fields.index(field.name) + 1)[0] def _get_field(model, name): lookup_fields = name.split(LOOKUP_SEP) prev_field = None opts = model._meta for field_name in lookup_fields: if field_name == "pk": field_name = opts.pk.name try: field = opts.get_field(field_name) except FieldDoesNotExist: # Ignore valid query lookups. if prev_field and prev_field.get_lookup(field_name): continue else: prev_field = field if hasattr(field, "get_path_info"): # This field is a relation, update opts to follow the relation path_info = field.get_path_info() opts = path_info[-1].to_opts return prev_field def is_in_lookup(name, value): return hasattr(value, "__iter__") & (name.split(LOOKUP_SEP)[-1] == "in") def _convert_in_lookup(model, field_name, options): """ ``in`` lookup can not be represented as keyword lookup. It requires transformation to combination of ``Q`` objects. Example: amount__in=[Money(10, 'EUR'), Money(5, 'USD)] is equivalent to: Q(amount=10, amount_currency='EUR') or Q(amount=5, amount_currency='USD') """ field = _get_field(model, field_name) new_query = Q() for value in options: if isinstance(value, MONEY_CLASSES): # amount__in=[Money(1, 'EUR'), Money(2, 'EUR')] option = {field.name: value.amount, get_currency_field_name(field.name, field): value.currency} elif isinstance(value, F): # amount__in=[Money(1, 'EUR'), F('another_money')] target_field = _get_field(model, value.name) option = { field.name: value, get_currency_field_name(field.name, field): F(get_currency_field_name(value.name, target_field)), } else: # amount__in=[1, 2, 3] option = {field.name: value} new_query |= Q(**option) return new_query def _expand_money_args(model, args): """ Augments args so that they contain _currency lookups - ie.. Q() | Q() """ for arg in args: if isinstance(arg, Q): _expand_arg(model, arg) return args def _expand_arg(model, arg): for i, child in enumerate(arg.children): if isinstance(child, Q): _expand_arg(model, child) elif isinstance(child, (list, tuple)): name, value = child field = _get_field(model, name) if isinstance(value, MONEY_CLASSES): clean_name = _get_clean_name(model, name) currency_field_name = get_currency_field_name(clean_name, field) arg.children[i] = Q(child, (currency_field_name, smart_str(value.currency))) if isinstance(field, MoneyField): if isinstance(value, (BaseExpression, F)): clean_name = _get_clean_name(model, name) if not isinstance(value, F): value = prepare_expression(value) if not _is_money_field(model, value, name): continue currency_field_name = get_currency_field_name(clean_name, field) target_field = _get_field(model, value.name) arg.children[i] = Q( child, (currency_field_name, F(get_currency_field_name(value.name, target_field))) ) if is_in_lookup(name, value): arg.children[i] = _convert_in_lookup(model, name, value) def _is_money_field(model, rhs, lhs_name): """ Checks if the target field from the expression is instance of MoneyField. """ # If the right side is the same field, then no reason to check if rhs.name == lhs_name: return True target_field = _get_field(model, rhs.name) return isinstance(target_field, MoneyField) def _expand_money_kwargs(model, args=(), kwargs=None, exclusions=()): """ Augments kwargs so that they contain _currency lookups. """ for name, value in list(kwargs.items()): if name in exclusions: continue field = _get_field(model, name) if isinstance(value, MONEY_CLASSES): clean_name = _get_clean_name(model, name) kwargs[name] = value.amount currency_field_name = get_currency_field_name(clean_name, field) kwargs[currency_field_name] = smart_str(value.currency) else: if isinstance(field, MoneyField): if isinstance(value, (BaseExpression, F)) and not isinstance(value, (Case, Cast, Coalesce)): clean_name = _get_clean_name(model, name) if not isinstance(value, F): value = prepare_expression(value) if not _is_money_field(model, value, name): continue currency_field_name = get_currency_field_name(clean_name, field) target_field = _get_field(model, value.name) kwargs[currency_field_name] = F(get_currency_field_name(value.name, target_field)) if is_in_lookup(name, value): args += (_convert_in_lookup(model, name, value),) del kwargs[name] elif isinstance(field, CurrencyField) and "defaults" in exclusions: _handle_currency_field(model, name, kwargs) return args, kwargs def _handle_currency_field(model, name, kwargs): name = _get_clean_name(model, name) field = _get_field(model, name) money_field = field.price_field if money_field.default is not NOT_PROVIDED and money_field.name not in kwargs: kwargs["defaults"] = kwargs.get("defaults", {}) kwargs["defaults"][money_field.name] = money_field.default.amount def _get_model(args, func): """ Returns the model class for given function. Note, that ``self`` is not available for proxy models. """ if hasattr(func, "__self__"): # Bound method model = func.__self__.model elif hasattr(func, "__wrapped__"): # Proxy model model = func.__wrapped__.__self__.model else: # Custom method on user-defined model manager. model = args[0].model return model def understands_money(func): """ Used to wrap a queryset method with logic to expand a query from something like: mymodel.objects.filter(money=Money(100, "USD")) To something equivalent to: mymodel.objects.filter(money=Decimal("100.0"), money_currency="USD") """ @wraps(func) def wrapper(*args, **kwargs): model = _get_model(args, func) args = _expand_money_args(model, args) exclusions = EXPAND_EXCLUSIONS.get(func.__name__, ()) args, kwargs = _expand_money_kwargs(model, args, kwargs, exclusions) queryset = func(*args, **kwargs) return add_money_comprehension_to_queryset(queryset) return wrapper RELEVANT_QUERYSET_METHODS = ("distinct", "get", "get_or_create", "filter", "exclude", "update", "order_by") EXPAND_EXCLUSIONS = {"get_or_create": ("defaults",)} def add_money_comprehension_to_queryset(qs): # Decorate each relevant method with understands_money in the queryset given for attr in RELEVANT_QUERYSET_METHODS: method = getattr(qs, attr, None) if method is not None: setattr(qs, attr, understands_money(method)) return qs def money_manager(manager): """ Patches a model manager's get_queryset method so that each QuerySet it returns is able to work on money fields. This allow users of django-money to use other managers while still doing money queries. """ # Need to dynamically subclass to add our behaviour, and then change # the class of 'manager' to our subclass. # Rejected alternatives: # # * A monkey patch that adds things to the manager instance dictionary. # This fails due to complications with Manager._copy_to_model behaviour. # # * Returning a new MoneyManager instance (rather than modifying # the passed in manager instance). This fails for reasons that # are tricky to get to the bottom of - Manager does funny things. class MoneyManager(manager.__class__): def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) return add_money_comprehension_to_queryset(queryset) manager.__class__ = MoneyManager return manager django-money/djmoney/models/validators.py000066400000000000000000000040201521124646200211160ustar00rootroot00000000000000from decimal import Decimal from django.core.exceptions import ValidationError from django.core.validators import BaseValidator from django.utils.translation import gettext_lazy as _ from djmoney.money import Money class BaseMoneyValidator(BaseValidator): def get_limit_value(self, cleaned): limit_value = self.limit_value() if callable(self.limit_value) else self.limit_value if isinstance(limit_value, Money): if cleaned.currency.code != limit_value.currency.code: return return limit_value elif isinstance(limit_value, (int, Decimal)): return limit_value try: return Money(limit_value[cleaned.currency.code], cleaned.currency.code) except KeyError: # There are no validation for this currency pass def __call__(self, value): cleaned = self.clean(value) limit_value = self.get_limit_value(cleaned) if limit_value is None: return if isinstance(limit_value, (int, Decimal)): cleaned = cleaned.amount params = {"limit_value": limit_value, "show_value": cleaned, "value": value} if self.compare(cleaned, limit_value): raise ValidationError(self.message, code=self.code, params=params) # The validators below are not inherited from Django's validators because of # subclass check in Django REST Framework, that removes all validators # from the serializer field except the first one in the `validators` list of each validator type (min / max) # Removing inheritance is the simplest option to make model validators work in REST Framework class MinMoneyValidator(BaseMoneyValidator): message = _("Ensure this value is greater than or equal to %(limit_value)s.") code = "min_value" def compare(self, a, b): return a < b class MaxMoneyValidator(BaseMoneyValidator): message = _("Ensure this value is less than or equal to %(limit_value)s.") code = "max_value" def compare(self, a, b): return a > b django-money/djmoney/money.py000066400000000000000000000130511521124646200166160ustar00rootroot00000000000000import warnings from types import MappingProxyType from django.conf import settings from django.db.models import F from django.utils import translation from django.utils.deconstruct import deconstructible from django.utils.html import avoid_wrapping, conditional_escape from django.utils.safestring import mark_safe import moneyed.l10n from moneyed import Currency, Money as DefaultMoney from .settings import DECIMAL_PLACES, MONEY_FORMAT __all__ = ["Money", "Currency"] @deconstructible class Money(DefaultMoney): """ Extends functionality of Money with Django-related features. """ use_l10n = None def __init__(self, *args, format_options=None, **kwargs): self.decimal_places = kwargs.pop("decimal_places", DECIMAL_PLACES) self.format_options = MappingProxyType(format_options) if format_options is not None else None super().__init__(*args, **kwargs) def _copy_attributes(self, source, target): """Copy attributes to the new `Money` instance. This class stores extra bits of information about string formatting that the parent class doesn't have. The problem is that the parent class creates new instances of `Money` without in some of its methods and it does so without knowing about `django-money`-level attributes. For this reason, when this class uses some methods of the parent class that have this behavior, the resulting instances lose those attribute values. When it comes to what number of decimal places to choose, we take the maximum number. """ selection = [ getattr(candidate, "decimal_places", None) for candidate in (self, source) if getattr(candidate, "decimal_places", None) is not None ] if selection: target.decimal_places = max(selection) def __add__(self, other): if isinstance(other, F): return other.__radd__(self) other = maybe_convert(other, self.currency) result = super().__add__(other) self._copy_attributes(other, result) return result def __sub__(self, other): if isinstance(other, F): return other.__rsub__(self) other = maybe_convert(other, self.currency) result = super().__sub__(other) self._copy_attributes(other, result) return result def __mul__(self, other): if isinstance(other, F): return other.__rmul__(self) result = super().__mul__(other) self._copy_attributes(other, result) return result def __truediv__(self, other): if isinstance(other, F): return other.__rtruediv__(self) result = super().__truediv__(other) if isinstance(result, self.__class__): self._copy_attributes(other, result) return result def __rtruediv__(self, other): # Backported from py-moneyed, non released bug-fix # https://github.com/py-moneyed/py-moneyed/blob/c518745dd9d7902781409daec1a05699799474dd/moneyed/classes.py#L217-L218 raise TypeError("Cannot divide non-Money by a Money instance.") @property def is_localized(self): if self.use_l10n is None: # This definitely raises a warning in Django 4 - we want to ignore RemovedInDjango50Warning # However, we cannot ignore this specific warning class as it doesn't exist in older # Django versions with warnings.catch_warnings(): warnings.filterwarnings("ignore") setting = getattr(settings, "USE_L10N", True) return setting return self.use_l10n def __str__(self): format_options = { **MONEY_FORMAT, **(self.format_options or {}), } locale = get_current_locale() if locale: format_options["locale"] = locale return moneyed.l10n.format_money(self, **format_options) def __html__(self): return mark_safe(avoid_wrapping(conditional_escape(str(self)))) def __round__(self, n=None): amount = round(self.amount, n) new = self.__class__(amount, self.currency) self._copy_attributes(self, new) return new def round(self, ndigits=0): new = super().round(ndigits) self._copy_attributes(self, new) return new def __pos__(self): new = super().__pos__() self._copy_attributes(self, new) return new def __neg__(self): new = super().__neg__() self._copy_attributes(self, new) return new def __abs__(self): new = super().__abs__() self._copy_attributes(self, new) return new def __rmod__(self, other): new = super().__rmod__(other) self._copy_attributes(self, new) return new # DefaultMoney sets those synonym functions # we overwrite the 'targets' so the wrong synonyms are called # Example: we overwrite __add__; __radd__ calls __add__ on DefaultMoney... __radd__ = __add__ __rmul__ = __mul__ def get_current_locale(): return translation.to_locale( translation.get_language() # get_language can return None starting from Django 1.8 or settings.LANGUAGE_CODE ) def maybe_convert(value, currency): """ Converts other Money instances to the local currency if `AUTO_CONVERT_MONEY` is set to True. """ if getattr(settings, "AUTO_CONVERT_MONEY", False) and value.currency != currency: from .contrib.exchange.models import convert_money return convert_money(value, currency) return value django-money/djmoney/money.pyi000066400000000000000000000042371521124646200167750ustar00rootroot00000000000000from __future__ import annotations from decimal import Decimal from typing import Any, Optional, Union, overload, type_check_only from django.db.models.expressions import CombinedExpression, F from django.utils.safestring import SafeText from typing_extensions import Literal, TypedDict from moneyed import Currency, Money as _DefaultMoney __all__ = ["Money", "Currency"] DefaultMoney = _DefaultMoney @type_check_only class _FormatOptions(TypedDict): "Format options parameter." format: Optional[str] locale: str currency_digits: bool format_type: Literal["standard", "standard:short", "name"] decimal_quantization: bool class Money(DefaultMoney): use_l10n: Any decimal_places: Any format_options: Any def __init__(self, *args, format_options: Optional[_FormatOptions] = ..., **kwargs) -> None: ... @overload # type: ignore[override] def __add__(self, other: Money) -> Money: ... @overload def __add__(self, other: F) -> CombinedExpression: ... @overload # type: ignore[override] def __sub__(self, other: Money) -> Money: ... @overload def __sub__(self, other: F) -> CombinedExpression: ... @overload # type: ignore[override] def __mul__(self, other: _NumericalType) -> Money: ... @overload def __mul__(self, other: F) -> CombinedExpression: ... @overload # type: ignore[override] def __truediv__(self, other: _NumericalType) -> Money: ... @overload def __truediv__(self, other: F) -> CombinedExpression: ... def __rtruediv__(self, other) -> None: ... # type: ignore[override] @property def is_localized(self) -> bool: ... def __html__(self) -> SafeText: ... def __round__(self, n: Any | None = ...) -> Money: ... def round(self, ndigits: int = ...) -> Money: ... # type: ignore[override] def __pos__(self) -> Money: ... def __neg__(self) -> Money: ... def __abs__(self) -> Money: ... def __rmod__(self, other) -> Money: ... def __radd__(self, other) -> Money: ... # type: ignore[override] def __rmul__(self, other) -> Money: ... # type: ignore[override] def get_current_locale() -> str: ... _NumericalType = Union[Decimal, float, int] django-money/djmoney/py.typed000066400000000000000000000000001521124646200166020ustar00rootroot00000000000000django-money/djmoney/serializers.py000066400000000000000000000047701521124646200200330ustar00rootroot00000000000000import json import sys import django from django.core.serializers.base import DeserializationError from django.core.serializers.json import Serializer as JSONSerializer from djmoney.money import Money from .models.fields import MoneyField from .utils import get_currency_field_name Serializer = JSONSerializer def Deserializer(stream_or_string, **options): # noqa """ Deserialize a stream or string of JSON data. """ # Local imports to allow using modified versions of `_get_model` / `_get_model_from_node` # It could be patched in runtime via `unittest.mock.patch` for example from django.core.serializers.python import Deserializer as PythonDeserializer if django.VERSION < (5, 2): from django.core.serializers.python import _get_model else: _get_model = PythonDeserializer._get_model_from_node ignore = options.pop("ignorenonexistent", False) if not isinstance(stream_or_string, (bytes, str)): stream_or_string = stream_or_string.read() if isinstance(stream_or_string, bytes): stream_or_string = stream_or_string.decode("utf-8") try: for obj in json.loads(stream_or_string): try: Model = _get_model(obj["model"]) except DeserializationError: if ignore: continue else: raise money_fields = {} fields = {} field_names = {field.name for field in Model._meta.get_fields()} for field_name, field_value in obj["fields"].items(): if ignore and field_name not in field_names: # skip fields no longer on model continue field = Model._meta.get_field(field_name) if isinstance(field, MoneyField) and field_value is not None: money_fields[field_name] = Money( field_value, obj["fields"][get_currency_field_name(field_name, field)] ) else: fields[field_name] = field_value obj["fields"] = fields for inner_obj in PythonDeserializer([obj], **options): for field, value in money_fields.items(): setattr(inner_obj.object, field, value) yield inner_obj except (GeneratorExit, DeserializationError): raise except Exception as exc: raise DeserializationError.with_traceback(DeserializationError(exc), sys.exc_info()[2]) django-money/djmoney/settings.py000066400000000000000000000037431521124646200173360ustar00rootroot00000000000000import operator from types import MappingProxyType from typing import Optional from django.conf import settings from moneyed import CURRENCIES, XXX, Currency # The default currency, you can define this in your project's settings module # This has to be a currency object imported from moneyed DEFAULT_CURRENCY: Optional[Currency] = getattr(settings, "DEFAULT_CURRENCY", None) # The default currency choices, you can define this in your project's # settings module PROJECT_CURRENCIES = getattr(settings, "CURRENCIES", None) CURRENCY_CHOICES = getattr(settings, "CURRENCY_CHOICES", None) if CURRENCY_CHOICES is None: if PROJECT_CURRENCIES: CURRENCY_CHOICES = [(code, CURRENCIES[code].name) for code in PROJECT_CURRENCIES] else: # 'XXX' is a backport from py-moneyed for transactions involving no currency; # this was previously referred to as moneyed.classes.DEFAULT_CURRENCY_CODE CURRENCY_CHOICES = [(c.code, c.name) for i, c in CURRENCIES.items() if c != XXX] CURRENCY_CHOICES.sort(key=operator.itemgetter(1, 0)) DECIMAL_PLACES = getattr(settings, "CURRENCY_DECIMAL_PLACES", 2) OPEN_EXCHANGE_RATES_URL = getattr(settings, "OPEN_EXCHANGE_RATES_URL", "https://openexchangerates.org/api/latest.json") OPEN_EXCHANGE_RATES_APP_ID = getattr(settings, "OPEN_EXCHANGE_RATES_APP_ID", None) FIXER_URL = getattr(settings, "FIXER_URL", "http://data.fixer.io/api/latest") FIXER_ACCESS_KEY = getattr(settings, "FIXER_ACCESS_KEY", None) UNIRATE_URL = getattr(settings, "UNIRATE_URL", "https://api.unirateapi.com/api/rates") UNIRATE_ACCESS_KEY = getattr(settings, "UNIRATE_ACCESS_KEY", None) BASE_CURRENCY = getattr(settings, "BASE_CURRENCY", "USD") EXCHANGE_BACKEND = getattr(settings, "EXCHANGE_BACKEND", "djmoney.contrib.exchange.backends.OpenExchangeRatesBackend") RATES_CACHE_TIMEOUT = getattr(settings, "RATES_CACHE_TIMEOUT", 600) CURRENCY_CODE_MAX_LENGTH = getattr(settings, "CURRENCY_CODE_MAX_LENGTH", 3) MONEY_FORMAT = MappingProxyType(getattr(settings, "MONEY_FORMAT", {})) django-money/djmoney/templatetags/000077500000000000000000000000001521124646200176075ustar00rootroot00000000000000django-money/djmoney/templatetags/__init__.py000066400000000000000000000000001521124646200217060ustar00rootroot00000000000000django-money/djmoney/templatetags/djmoney.py000066400000000000000000000065011521124646200216300ustar00rootroot00000000000000from decimal import Decimal from django import template from django.template import TemplateSyntaxError from ..money import Money from ..utils import MONEY_CLASSES register = template.Library() class MoneyLocalizeNode(template.Node): def __repr__(self): return "" % (self.money.amount, self.money.currency) def __init__(self, money=None, amount=None, currency=None, use_l10n=None, var_name=None): if money and (amount or currency): raise Exception('You can define either "money" or the "amount" and "currency".') self.money = money self.amount = amount self.currency = currency self.use_l10n = use_l10n self.var_name = var_name @classmethod def handle_token(cls, parser, token): tokens = token.contents.split() # default value var_name = None use_l10n = True # GET variable var_name if len(tokens) > 3: if tokens[-2] == "as": var_name = parser.compile_filter(tokens[-1]) # remove the already used data tokens = tokens[0:-2] # GET variable use_l10n if tokens[-1].lower() in ("on", "off"): use_l10n = tokens[-1].lower() == "on" # remove the already used data tokens.pop(-1) # GET variable money if len(tokens) == 2: return cls(money=parser.compile_filter(tokens[1]), var_name=var_name, use_l10n=use_l10n) # GET variable amount and currency if len(tokens) == 3: return cls( amount=parser.compile_filter(tokens[1]), currency=parser.compile_filter(tokens[2]), var_name=var_name, use_l10n=use_l10n, ) raise TemplateSyntaxError("Wrong number of input data to the tag.") def render(self, context): money = self.money.resolve(context) if self.money else None amount = self.amount.resolve(context) if self.amount else None currency = self.currency.resolve(context) if self.currency else None if money is not None: if not isinstance(money, MONEY_CLASSES): raise TemplateSyntaxError('The variable "money" must be an instance of Money.') elif amount is not None and currency is not None: money = Money(Decimal(str(amount)), str(currency)) else: raise TemplateSyntaxError("You must define both variables: amount and currency.") money.use_l10n = self.use_l10n if self.var_name is None: return str(money) # as context[self.var_name.token] = money return "" @register.tag def money_localize(parser, token): """ Usage:: {% money_localize [ on(default) | off ] [as var_name] %} {% money_localize [ on(default) | off ] [as var_name] %} Example: The same effect: {% money_localize money_object %} {% money_localize money_object on %} Assignment to a variable: {% money_localize money_object on as NEW_MONEY_OBJECT %} Formatting the number with currency: {% money_localize '4.5' 'USD' %} Return:: Money object """ return MoneyLocalizeNode.handle_token(parser, token) django-money/djmoney/utils.py000066400000000000000000000020621521124646200166270ustar00rootroot00000000000000from django.db.models import F from django.db.models.expressions import BaseExpression from djmoney.money import Money from moneyed import Money as OldMoney MONEY_CLASSES = (Money, OldMoney) def get_currency_field_name(name, field=None): if field and getattr(field, "currency_field_name", None): return field.currency_field_name return "%s_currency" % name def get_amount(value): """ Extracts decimal value from Money or Expression. """ if isinstance(value, MONEY_CLASSES): return value.amount elif isinstance(value, BaseExpression) and not isinstance(value, F): return get_amount(value.value) return value def prepare_expression(expr): """ Prepares some complex money expression to be used in query. """ if isinstance(expr.rhs, F): # Money(...) + F('money') target, return_value = expr.lhs, expr.rhs else: # F('money') + Money(...) target, return_value = expr.rhs, expr.lhs amount = get_amount(target) target.value = amount return return_value django-money/docs/000077500000000000000000000000001521124646200144005ustar00rootroot00000000000000django-money/docs/Makefile000066400000000000000000000170001521124646200160360ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-money.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-money.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-money" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-money" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." django-money/docs/_static/000077500000000000000000000000001521124646200160265ustar00rootroot00000000000000django-money/docs/_static/.gitkeep000066400000000000000000000000001521124646200174450ustar00rootroot00000000000000django-money/docs/changes.rst000066400000000000000000001123661521124646200165530ustar00rootroot00000000000000Changelog ========= `Unreleased`_ ------------- ... `3.6.1`_ - 2026-06-07 --------------------- **Added** - ``UniRateBackend`` exchange-rate backend for `UniRateAPI `_. Configure via ``EXCHANGE_BACKEND = "djmoney.contrib.exchange.backends.UniRateBackend"`` and set ``UNIRATE_ACCESS_KEY``. **Changed** - Upsert rates instead of deleting and recreating them :github-issue:`818` (:github-user:`2tunnels`) - Update README to reflect currently supported Python (3.10–3.13) and Django (4.2–5.2) versions. :github-issue:`820` (:github-user:`smhtbtb`) `3.6`_ - 2026-01-27 ------------------- **Added** - Support for callable ``default`` and ``currency_default``, and ``currency_choices`` :github-issue:`683` (:github-user:`benjaoming`) - Add ``backend`` kwarg to ``convert_money`` function :github-issue:`787` (:github-user:`heckad`) - Add getting a limit value from a callable :github-issue:`810` (:github-user:`heckad`) **Fixed** - The auto-generated hidden input field that tracks initial data in formsets with MoneyField should now work :github-issue:`803` (:github-user:`benjaoming`) **Changed** - Migrate to pyproject.toml and uv :github-issue:`791` (:github-user:`browniebroke`) **Removed** - Python 3.8 and 3.9 support was dropped to give way for typehints :github-issue:`781` (:github-user:`benjaoming`) `3.5.4`_ - 2025-04-17 --------------------- **Added** - Django 5.2 and Python 3.13 support :github-issue:`785` (:github-user:`gvangool` and :github-user:`browniebroke`) `3.5.3`_ - 2024-08-01 --------------------- **Fixed** - django-rest-framework: MoneyField does not work anymore with custom serializer fields :github-issue:`768` (:github-user:`apjama`) **Added** - Django 5.1 support :github-issue:`767` (:github-user:`benjaoming`) `3.5.2`_ - 2024-05-07 --------------------- **Fixed** - django-rest-framework: Fix regression from 3.5 :github-issue:`762` (:github-user:`dariusmazeika`) `3.5.1`_ - 2024-05-05 --------------------- **Fixed** - django-rest-framework: Fix regression from 3.5 :github-issue:`757` (:github-user:`phillipuniverse`) - Add `base` parameter to openexchangerates.org backend :github-issue:`751` (:github-user:`foarsitter`) `3.5`_ - 2024-05-04 ------------------- .. important:: If you generated ``MoneyField`` migrations in the previous series 3.4.x, you may have to manually edit subsequent migrations. Please share your successful approaches in :github-issue:`731`. **Fixed** - Revert 3.4 patch, meaning that auto-generated CurrencyField is once again part of migrations :github-issue:`731` (:github-user:`benjaoming`) - django-rest-framework: MinMoneyValidator and MaxMoneyValidator fixed, may require default_currency defined :github-issue:`722` (:github-user:`hosamhamdy258` :github-user:`errietta` :github-user:`benjaoming`) **Added** - Django 5.0 support :github-issue:`753` (:github-user:`benjaoming`) **Removed** - Official support for Django 2.2, 3.2, 4.0, 4.1 :github-issue:`753` (:github-user:`benjaoming`) `3.4.1`_ - 2023-11-30 --------------------- **Fixed** - The default setting for ``CURRENCY_CHOICES`` excluded the currency choice defined by ``DEFAULT_CURRENCY``. :github-issue:`739` (:github-user:`Naggafin`) `3.4`_ - 2023-10-17 ------------------- .. note:: If you are using Django REST Framework or any other mechanism that relies on a custom serializer, please note that you now manually have to register the serializer. See :ref:`this code snippet `. **Changed** - Don't register Django Money serializers by default, instead the user should actively register a serializer in the ``settings.py`` :github-issue:`636` (:github-user:`emorozov`) `3.3`_ - 2023-09-10 ------------------- **Fixed** - Fix detection of necessary migrations. Note that this means that previously undetected migrations will be detected as of this version :github-issue:`725` (:github-user:`vanschelven`) `3.2`_ - 2023-07-03 ------------------- **Changed** - Explicitly define ``id`` in ``djmoney.contrib.exchange.Rate`` model - This ensures that the database table will use ``AutoField`` even if ``DEFAULT_AUTO_FIELD`` is set to ``BigAutoField`` in the Django project's settings #716 **Fixed** - Downgrade asgiref to 3.6 to work with pypy3 `3.1`_ - 2023-04-20 ------------------- **Added** - Python 3.11 support :github-issue:`700` (:github-user:`sdarmofal`) - Django 4.2 support :github-issue:`700` (:github-user:`sdarmofal`) - Pyright support for .pyi files :github-issue:`686` (:github-user:`karolyi`) - Support for ``Coalesce`` :github-issue:`678` (:github-user:`stianjensen`) **Fixed** - Support for ``Money`` type with ``Coalesce`` in ``QuerySet.update()`` :github-issue:`678` (:github-user:`stianjensen`) - pre-commit config for moved flake8 repo (:github-user:`sdarmofal`) - Use latest setup-python GitHub Action :github-issue:`692` (:github-user:`sondrelg`) - Optimize: Rate is always 1 if source and target are equal :github-issue:`689` (:github-user:`nschlemm`) - Fixer.io backend: Avoid 403 errors :github-issue:`681` (:github-user:`spaut33`) `3.0`_ - 2022-06-20 -------------------- **Changed** - Update py-moneyed to 2.0. :github-issue:`638` (:github-user:`antonagestam`, :github-user:`flaeppe`, :github-user:`paoloxnet`) - Remove the deprecated ``Money.decimal_places_display`` property and argument. :github-issue:`638` (:github-user:`antonagestam`, :github-user:`flaeppe`, :github-user:`paoloxnet`) - Remove the deprecated ``CURRENCY_DECIMAL_PLACES_DISPLAY`` setting. :github-issue:`638` (:github-user:`antonagestam`, :github-user:`flaeppe`, :github-user:`paoloxnet`) - Null constraint on an implicit ``CurrencyField`` is now declared from ``null=...`` argument to ``MoneyField``. :github-issue:`638` (:github-user:`antonagestam`, :github-user:`flaeppe`, :github-user:`paoloxnet`) **Fixed** - Improve the internal check for whether a currency is provided :github-issue:`657` (:github-user:`davidszotten`) - Fix test suite for django main branch :github-issue:`657` (:github-user:`davidszotten`) - ``MoneyField`` raises ``TypeError`` when default contains a valid amount but no currence, i.e. ``Money(123, None)``. :github-issue:`661` (:github-user:`flaeppe`) - ``MoneyField`` supports default of type ``bytes`` :github-issue:`661` (:github-user:`flaeppe`) **Added** - Add support for Django 4.0 and 4.1. - Add support for Python 3.10. **Removed** - Drop support for Django 3.1. - Drop support for Python 3.6. `2.1.1`_ - 2022-01-02 --------------------- **Changed** - Renamed ``master`` branch to ``main`` (:github-user:`benjaoming`) **Fixed** - Make Django REST Framework integration always raise lower-level errors as ``ValidationError``. :github-issue:`601`, :github-issue:`637` (:github-user:`flaeppe`) - False positives in Migration changes, improvements to ``MoneyField.deconstruct``. :github-issue:`646`, :github-issue:`648` (:github-user:`flaeppe`) `2.1`_ - 2021-09-17 ------------------- **Added** - Add support for Django 3.2. :github-issue:`612` (:github-user:`antonagestam`) **Removed** - Drop support for Django 1.11, 2.1 and 3.0. :github-issue:`612` (:github-user:`antonagestam`) - Drop support for Python 3.5. :github-issue:`612` (:github-user:`antonagestam`) `2.0.3`_ - 2021-09-04 --------------------- **Fixed** - Inconsistent ``Money._copy_attributes`` behaviour when non-``Money`` instances are involved. :github-issue:`630` (:github-user:`tned73`) `2.0.2`_ - 2021-09-04 --------------------- **Fixed** - Inconsistent ``Money._copy_attributes`` behaviour. :github-issue:`629` (:github-user:`tned73`) `2.0.1`_ - 2021-07-09 --------------------- **Fixed** - Invalid deprecation warning behavior. :github-issue:`624` (:github-user:`nerdoc`) `2.0`_ - 2021-05-23 ------------------- **Added** - New setting ``CURRENCY_CODE_MAX_LENGTH`` configures default max_length for MoneyField and ``exchange`` app models. **Changed** - BREAKING: Update ``py-moneyed`` to ``>=1.2,<2``. It uses ``babel`` to format ``Money``, which formats it differently than ``py-moneyed<1``. :github-issue:`567` (:github-user:`antonagestam`) **Deprecated** - ``Money.decimal_places_display`` will be removed in django-money 3.0. - ``CURRENCY_DECIMAL_PLACES_DISPLAY`` will be removed in django-money 3.0. `1.3.1`_ - 2021-02-04 --------------------- **Fixed** - Do not mutate the input ``moneyed.Money`` class to ``djmoney.money.Money`` in ``MoneyField.default`` and F-expressions. :github-issue:`603` (:github-user:`moser`) `1.3`_ - 2021-01-10 ------------------- **Added** - Improved localization: New setting ``CURRENCY_DECIMAL_PLACES_DISPLAY`` configures decimal places to display for each configured currency. :github-issue:`521` (:github-user:`wearebasti`) **Changed** - Set the default value for ``models.fields.MoneyField`` to ``NOT_PROVIDED``. (:github-user:`tned73`) **Fixed** - Pin ``pymoneyed<1.0`` as it changed the ``repr`` output of the ``Money`` class. (:github-user:`Stranger6667`) - Subtracting ``Money`` from ``moneyed.Money``. Regression, introduced in ``1.2``. :github-issue:`593` (:github-user:`Stranger6667`) - Missing the right ``Money.decimal_places`` and ``Money.decimal_places_display`` values after some arithmetic operations. :github-issue:`595` (:github-user:`Stranger6667`) `1.2.2`_ - 2020-12-29 --------------------- **Fixed** - Confusing "number-over-money" division behavior by backporting changes from ``py-moneyed``. :github-issue:`586` (:github-user:`wearebasti`) - ``AttributeError`` when a ``Money`` instance is divided by ``Money``. :github-issue:`585` (:github-user:`niklasb`) `1.2.1`_ - 2020-11-29 --------------------- **Fixed** - Aggregation through a proxy model. :github-issue:`583` (:github-user:`tned73`) `1.2`_ - 2020-11-26 ------------------- **Fixed** - Resulting Money object from arithmetics (add / sub / ...) inherits maximum decimal_places from arguments :github-issue:`522` (:github-user:`wearebasti`) - ``DeprecationWarning`` related to the usage of ``cafile`` in ``urlopen``. :github-issue:`553` (:github-user:`Stranger6667`) **Added** - Django 3.1 support `1.1`_ - 2020-04-06 ------------------- **Fixed** - Optimize money operations on MoneyField instances with the same currencies. :github-issue:`541` (:github-user:`horpto`) **Added** - Support for ``Money`` type in ``QuerySet.bulk_update()`` :github-issue:`534` (:github-user:`satels`) `1.0`_ - 2019-11-08 ------------------- **Added** - Support for money descriptor customization. (:github-user:`Stranger6667`) - Fix ``order_by()`` not returning money-compatible queryset :github-issue:`519` (:github-user:`lieryan`) - Django 3.0 support **Removed** - Support for Django 1.8 & 2.0. (:github-user:`Stranger6667`) - Support for Python 2.7. :github-issue:`515` (:github-user:`benjaoming`) - Support for Python 3.4. (:github-user:`Stranger6667`) - ``MoneyPatched``, use ``djmoney.money.Money`` instead. (:github-user:`Stranger6667`) **Fixed** - Support instances with ``decimal_places=0`` :github-issue:`509` (:github-user:`fara`) `0.15.1`_ - 2019-06-22 ---------------------- **Fixed** - Respect field ``decimal_places`` when instantiating ``Money`` object from field db values. :github-issue:`501` (:github-user:`astutejoe`) - Restored linting in CI tests (:github-user:`benjaoming`) `0.15`_ - 2019-05-30 -------------------- .. warning:: This release contains backwards incompatibility, please read the release notes below. Backwards incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Remove implicit default value on non-nullable MoneyFields. Backwards incompatible change: set explicit ``default=0.0`` to keep previous behavior. :github-issue:`411` (:github-user:`washeck`) - Remove support for calling ``float`` on ``Money`` instances. Use the ``amount`` attribute instead. (:github-user:`Stranger6667`) - ``MinMoneyValidator`` and ``MaxMoneyValidator`` are not inherited from Django's ``MinValueValidator`` and ``MaxValueValidator`` anymore. :github-issue:`376` - In model and non-model forms ``forms.MoneyField`` uses ``CURRENCY_DECIMAL_PLACES`` as the default value for ``decimal_places``. :github-issue:`434` (:github-user:`Stranger6667`, :github-user:`andytwoods`) **Added** - Add ``Money.decimal_places`` for per-instance configuration of decimal places in the string representation. - Support for customization of ``CurrencyField`` length. Some cryptocurrencies could have codes longer than three characters. :github-issue:`480` (:github-user:`Stranger6667`, :github-user:`MrFus10n`) - Add ``default_currency`` option for REST Framework field. :github-issue:`475` (:github-user:`butorov`) **Fixed** - Failing certificates checks when accessing 3rd party exchange rates backends. Fixed by adding `certifi` to the dependencies list. :github-issue:`403` (:github-user:`Stranger6667`) - Fixed model-level ``validators`` behavior in REST Framework. :github-issue:`376` (:github-user:`rapIsKal`, :github-user:`Stranger6667`) - Setting keyword argument ``default_currency=None`` for ``MoneyField`` did not revert to ``settings.DEFAULT_CURRENCY`` and set ``str(None)`` as database value for currency. :github-issue:`490` (:github-user:`benjaoming`) **Changed** - Allow using patched ``django.core.serializers.python._get_model`` in serializers, which could be helpful for migrations. (:github-user:`Formulka`, :github-user:`Stranger6667`) `0.14.4`_ - 2019-01-07 ---------------------- **Changed** - Re-raise arbitrary exceptions in JSON deserializer as `DeserializationError`. (:github-user:`Stranger6667`) **Fixed** - Invalid Django 1.8 version check in ``djmoney.models.fields.MoneyField.value_to_string``. (:github-user:`Stranger6667`) - InvalidOperation in ``djmoney.contrib.django_rest_framework.fields.MoneyField.get_value`` when amount is None and currency is not None. :github-issue:`458` (:github-user:`carvincarl`) `0.14.3`_ - 2018-08-14 ---------------------- **Fixed** - ``djmoney.forms.widgets.MoneyWidget`` decompression on Django 2.1+. :github-issue:`443` (:github-user:`Stranger6667`) `0.14.2`_ - 2018-07-23 ---------------------- **Fixed** - Validation of ``djmoney.forms.fields.MoneyField`` when ``disabled=True`` is passed to it. :github-issue:`439` (:github-user:`stinovlas`, :github-user:`Stranger6667`) `0.14.1`_ - 2018-07-17 ---------------------- **Added** - Support for indirect rates conversion through maximum 1 extra step (when there is no direct conversion rate: converting by means of a third currency for which both source and target currency have conversion rates). :github-issue:`425` (:github-user:`Stranger6667`, :github-user:`77cc33`) **Fixed** - Error was raised when trying to do a query with a `ModelWithNullableCurrency`. :github-issue:`427` (:github-user:`Woile`) `0.14`_ - 2018-06-09 -------------------- **Added** - Caching of exchange rates. :github-issue:`398` (:github-user:`Stranger6667`) - Added support for nullable ``CurrencyField``. :github-issue:`260` (:github-user:`Stranger6667`) **Fixed** - Same currency conversion getting MissingRate exception :github-issue:`418` (:github-user:`humrochagf`) - `TypeError` during templatetag usage inside a for loop on Django 2.0. :github-issue:`402` (:github-user:`f213`) **Removed** - Support for Python 3.3 :github-issue:`410` (:github-user:`benjaoming`) - Deprecated ``choices`` argument from ``djmoney.forms.fields.MoneyField``. Use ``currency_choices`` instead. (:github-user:`Stranger6667`) `0.13.5`_ - 2018-05-19 ---------------------- **Fixed** - Missing in dist, ``djmoney/__init__.py``. :github-issue:`417` (:github-user:`benjaoming`) `0.13.4`_ - 2018-05-19 ---------------------- **Fixed** - Packaging of ``djmoney.contrib.exchange.management.commands``. :github-issue:`412` (:github-user:`77cc33`, :github-user:`Stranger6667`) `0.13.3`_ - 2018-05-12 ---------------------- **Added** - Rounding support via ``round`` built-in function on Python 3. (:github-user:`Stranger6667`) `0.13.2`_ - 2018-04-16 ---------------------- **Added** - Django Admin integration for exchange rates. :github-issue:`392` (:github-user:`Stranger6667`) **Fixed** - Exchange rates. TypeError when decoding JSON on Python 3.3-3.5. :github-issue:`399` (:github-user:`kcyeu`) - Managers patching for models with custom ``Meta.default_manager_name``. :github-issue:`400` (:github-user:`Stranger6667`) `0.13.1`_ - 2018-04-07 ---------------------- **Fixed** - Regression: Could not run w/o ``django.contrib.exchange`` :github-issue:`388` (:github-user:`Stranger6667`) `0.13`_ - 2018-04-07 -------------------- **Added** - Currency exchange :github-issue:`385` (:github-user:`Stranger6667`) **Removed** - Support for ``django-money-rates`` :github-issue:`385` (:github-user:`Stranger6667`) - Deprecated ``Money.__float__`` which is implicitly called on some ``sum()`` operations :github-issue:`347`. (:github-user:`jonashaag`) Migration from django-money-rates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The new application is a drop-in replacement for ``django-money-rates``. To migrate from ``django-money-rates``: - In ``INSTALLED_APPS`` replace ``djmoney_rates`` with ``djmoney.contrib.exchange`` - Set ``OPEN_EXCHANGE_RATES_APP_ID`` setting with your app id - Run ``python manage.py migrate`` - Run ``python manage.py update_rates`` For more information, look at ``Working with Exchange Rates`` section in README. `0.12.3`_ - 2017-12-13 ---------------------- **Fixed** - Fixed ``BaseMoneyValidator`` with falsy limit values. :github-issue:`371` (:github-user:`1337`) `0.12.2`_ - 2017-12-12 ---------------------- **Fixed** - Django master branch compatibility. :github-issue:`361` (:github-user:`Stranger6667`) - Fixed ``get_or_create`` for models with shared currency. :github-issue:`364` (:github-user:`Stranger6667`) **Changed** - Removed confusing rounding to integral value in ``Money.__repr__``. :github-issue:`366` (:github-user:`Stranger6667`, :github-user:`evenicoulddoit`) `0.12.1`_ - 2017-11-20 ---------------------- **Fixed** - Fixed migrations on SQLite. :github-issue:`139`, :github-issue:`338` (:github-user:`Stranger6667`) - Fixed ``Field.rel.to`` usage for Django 2.0. :github-issue:`349` (:github-user:`richardowen`) - Fixed Django REST Framework behaviour for serializers without ``*_currency`` field in serializer's ``Meta.fields``. :github-issue:`351` (:github-user:`elcolie`, :github-user:`Stranger6667`) `0.12`_ - 2017-10-22 -------------------- **Added** - Ability to specify name for currency field. :github-issue:`195` (:github-user:`Stranger6667`) - Validators for ``MoneyField``. :github-issue:`308` (:github-user:`Stranger6667`) **Changed** - Improved ``Money`` support. Now ``django-money`` fully relies on ``pymoneyed`` localization everywhere, including Django admin. :github-issue:`276` (:github-user:`Stranger6667`) - Implement ``__html__`` method. If used in Django templates, an ``Money`` object's amount and currency are now separated with non-breaking space (`` ``) :github-issue:`337` (:github-user:`jonashaag`) **Deprecated** - ``djmoney.models.fields.MoneyPatched`` and ``moneyed.Money`` are deprecated. Use ``djmoney.money.Money`` instead. **Fixed** - Fixed model field validation. :github-issue:`308` (:github-user:`Stranger6667`). - Fixed managers caching for Django >= 1.10. :github-issue:`318` (:github-user:`Stranger6667`). - Fixed ``F`` expressions support for ``in`` lookups. :github-issue:`321` (:github-user:`Stranger6667`). - Fixed money comprehension on querysets. :github-issue:`331` (:github-user:`Stranger6667`, :github-user:`jaavii1988`). - Fixed errors in Django Admin integration. :github-issue:`334` (:github-user:`Stranger6667`, :github-user:`adi-`). **Removed** - Dropped support for Python 2.6 and 3.2. (:github-user:`Stranger6667`) - Dropped support for Django 1.4, 1.5, 1.6, 1.7 and 1.9. (:github-user:`Stranger6667`) `0.11.4`_ - 2017-06-26 ---------------------- **Fixed** - Fixed money parameters processing in update queries. :github-issue:`309` (:github-user:`Stranger6667`) `0.11.3`_ - 2017-06-19 ---------------------- **Fixed** - Restored support for Django 1.4, 1.5, 1.6, and 1.7 & Python 2.6 :github-issue:`304` (:github-user:`Stranger6667`) `0.11.2`_ - 2017-05-31 ---------------------- **Fixed** - Fixed field lookup regression. :github-issue:`300` (:github-user:`lmdsp`, :github-user:`Stranger6667`) `0.11.1`_ - 2017-05-26 ---------------------- **Fixed** - Fixed access to models properties. :github-issue:`297` (:github-user:`mithrilstar`, :github-user:`Stranger6667`) **Removed** - Dropped support for Python 2.6. (:github-user:`Stranger6667`) - Dropped support for Django < 1.8. (:github-user:`Stranger6667`) `0.11`_ - 2017-05-19 -------------------- **Added** - An ability to set custom currency choices via ``CURRENCY_CHOICES`` settings option. :github-issue:`211` (:github-user:`Stranger6667`, :github-user:`ChessSpider`) **Fixed** - Fixed ``AttributeError`` in ``get_or_create`` when the model have no default. :github-issue:`268` (:github-user:`Stranger6667`, :github-user:`lobziik`) - Fixed ``UnicodeEncodeError`` in string representation of ``MoneyPatched`` on Python 2. :github-issue:`272` (:github-user:`Stranger6667`) - Fixed various displaying errors in Django Admin . :github-issue:`232`, :github-issue:`220`, :github-issue:`196`, :github-issue:`102`, :github-issue:`90` (:github-user:`Stranger6667`, :github-user:`arthurk`, :github-user:`mstarostik`, :github-user:`eriktelepovsky`, :github-user:`jplehmann`, :github-user:`graik`, :github-user:`benjaoming`, :github-user:`k8n`, :github-user:`yellow-sky`) - Fixed non-Money values support for ``in`` lookup. :github-issue:`278` (:github-user:`Stranger6667`) - Fixed available lookups with removing of needless lookup check. :github-issue:`277` (:github-user:`Stranger6667`) - Fixed compatibility with ``py-moneyed``. (:github-user:`Stranger6667`) - Fixed ignored currency value in Django REST Framework integration. :github-issue:`292` (:github-user:`gonzalobf`) `0.10.2`_ - 2017-02-18 ---------------------- **Added** - Added ability to configure decimal places output. :github-issue:`154`, :github-issue:`251` (:github-user:`ivanchenkodmitry`) **Fixed** - Fixed handling of ``defaults`` keyword argument in ``get_or_create`` method. :github-issue:`257` (:github-user:`kjagiello`) - Fixed handling of currency fields lookups in ``get_or_create`` method. :github-issue:`258` (:github-user:`Stranger6667`) - Fixed ``PendingDeprecationWarning`` during form initialization. :github-issue:`262` (:github-user:`Stranger6667`, :github-user:`spookylukey`) - Fixed handling of ``F`` expressions which involve non-Money fields. :github-issue:`265` (:github-user:`Stranger6667`) `0.10.1`_ - 2016-12-26 ---------------------- **Fixed** - Fixed default value for ``djmoney.forms.fields.MoneyField``. :github-issue:`249` (:github-user:`tsouvarev`) `0.10`_ - 2016-12-19 -------------------- **Changed** - Do not fail comparisons because of different currency. Just return ``False`` :github-issue:`225` (:github-user:`benjaoming` and :github-user:`ivirabyan`) **Fixed** - Fixed ``understands_money`` behaviour. Now it can be used as a decorator :github-issue:`215` (:github-user:`Stranger6667`) - Fixed: Not possible to revert MoneyField currency back to default :github-issue:`221` (:github-user:`benjaoming`) - Fixed invalid ``creation_counter`` handling. :github-issue:`235` (:github-user:`msgre` and :github-user:`Stranger6667`) - Fixed broken field resolving. :github-issue:`241` (:github-user:`Stranger6667`) `0.9.1`_ - 2016-08-01 --------------------- **Fixed** - Fixed packaging. `0.9.0`_ - 2016-07-31 --------------------- NB! If you are using custom model managers **not** named ``objects`` and you expect them to still work, please read below. **Added** - Support for ``Value`` and ``Func`` expressions in queries. (:github-user:`Stranger6667`) - Support for ``in`` lookup. (:github-user:`Stranger6667`) - Django REST Framework support. :github-issue:`179` (:github-user:`Stranger6667`) - Django 1.10 support. :github-issue:`198` (:github-user:`Stranger6667`) - Improved South support. (:github-user:`Stranger6667`) **Changed** - Changed auto conversion of currencies using djmoney_rates (added in 0.7.3) to be off by default. You must now add ``AUTO_CONVERT_MONEY = True`` in your ``settings.py`` if you want this feature. :github-issue:`199` (:github-user:`spookylukey`) - Only make ``objects`` a MoneyManager instance automatically. :github-issue:`194` and :github-issue:`201` (:github-user:`inureyes`) **Fixed** - Fixed default currency value for nullable fields in forms. :github-issue:`138` (:github-user:`Stranger6667`) - Fixed ``_has_changed`` deprecation warnings. :github-issue:`206` (:github-user:`Stranger6667`) - Fixed ``get_or_create`` crash, when ``defaults`` is passed. :github-issue:`213` (:github-user:`Stranger6667`, :github-user:`spookylukey`) Note about automatic model manager patches ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In 0.8, Django-money automatically patches every model managers with ``MoneyManager``. This causes migration problems if two or more managers are used in the same model. As a side effect, other managers are also finally wrapped with ``MoneyManager``. This effect leads Django migration to point to fields with other managers to ``MoneyManager``, and raises ``ValueError`` (``MoneyManager`` only exists as a return of ``money_manager``, not a class-form. However migration procedure tries to find ``MoneyManager`` to patch other managers.) From 0.9, Django-money only patches ``objects`` with ``MoneyManager`` by default (as documented). To patch other managers (e.g. custom managers), patch them by wrapping with ``money_manager``. .. code-block:: python 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()) `0.8`_ - 2016-04-23 ------------------- **Added** - Support for serialization of ``MoneyPatched`` instances in migrations. (:github-user:`AlexRiina`) - Improved django-money-rates support. :github-issue:`173` (:github-user:`Stranger6667`) - Extended ``F`` expressions support. (:github-user:`Stranger6667`) - Pre-commit hooks support. (:github-user:`benjaoming`) - Isort integration. (:github-user:`Stranger6667`) - Makefile for common commands. (:github-user:`Stranger6667`) - Codecov.io integration. (:github-user:`Stranger6667`) - Python 3.5 builds to tox.ini and travis.yml. (:github-user:`Stranger6667`) - Django master support. (:github-user:`Stranger6667`) - Python 3.2 compatibility. (:github-user:`Stranger6667`) **Changed** - Refactored test suite (:github-user:`Stranger6667`) **Fixed** - Fixed fields caching. :github-issue:`186` (:github-user:`Stranger6667`) - Fixed m2m fields data loss on Django < 1.8. :github-issue:`184` (:github-user:`Stranger6667`) - Fixed managers access via instances. :github-issue:`86` (:github-user:`Stranger6667`) - Fixed currency handling behaviour. :github-issue:`172` (:github-user:`Stranger6667`) - Many PEP8 & flake8 fixes. (:github-user:`benjaoming`) - Fixed filtration with ``F`` expressions. :github-issue:`174` (:github-user:`Stranger6667`) - Fixed querying on Django 1.8+. :github-issue:`166` (:github-user:`Stranger6667`) `0.7.6`_ - 2016-01-08 --------------------- **Added** - Added correct paths for py.test discovery. (:github-user:`benjaoming`) - Mention Django 1.9 in tox.ini. (:github-user:`benjaoming`) **Fixed** - Fix for ``get_or_create`` / ``create`` manager methods not respecting currency code. (:github-user:`toudi`) - Fix unit tests. (:github-user:`toudi`) - Fix for using ``MoneyField`` with ``F`` expressions when using Django >= 1.8. (:github-user:`toudi`) `0.7.5`_ - 2015-12-22 --------------------- **Fixed** - Fallback to ``_meta.fields`` if ``_meta.get_fields`` raises ``AttributeError`` :github-issue:`149` (:github-user:`browniebroke`) - pip instructions updated. (:github-user:`GheloAce`) `0.7.4`_ - 2015-11-02 --------------------- **Added** - Support for Django 1.9 (:github-user:`kjagiello`) **Fixed** - Fixed loaddata. (:github-user:`jack-cvr`) - Python 2.6 fixes. (:github-user:`jack-cvr`) - Fixed currency choices ordering. (:github-user:`synotna`) `0.7.3`_ - 2015-10-16 --------------------- **Added** - Sum different currencies. (:github-user:`dnmellen`) - ``__eq__`` method. (:github-user:`benjaoming`) - Comparison of different currencies. (:github-user:`benjaoming`) - Default currency. (:github-user:`benjaoming`) **Fixed** - Fix using Choices for setting currency choices. (:github-user:`benjaoming`) - Fix tests for Python 2.6. (:github-user:`plumdog`) `0.7.2`_ - 2015-09-01 --------------------- **Fixed** - Better checks on ``None`` values. (:github-user:`tsouvarev`, :github-user:`sjdines`) - Consistency with South declarations and calling ``str`` function. (:github-user:`sjdines`) `0.7.1`_ - 2015-08-11 --------------------- **Fixed** - Fix bug in printing ``MoneyField``. (:github-user:`YAmikep`) - Added fallback value for current locale getter. (:github-user:`sjdines`) `0.7.0`_ - 2015-06-14 --------------------- **Added** - Django 1.8 compatibility. (:github-user:`willhcr`) `0.6.0`_ - 2015-05-23 --------------------- **Added** - Python 3 trove classifier. (:github-user:`dekkers`) **Changed** - Tox cleanup. (:github-user:`edwinlunando`) - Improved ``README``. (:github-user:`glarrain`) - Added/Cleaned up tests. (:github-user:`spookylukey`, :github-user:`AlexRiina`) **Fixed** - Append ``_currency`` to non-money ExpressionFields. :github-issue:`101` (:github-user:`alexhayes`, :github-user:`AlexRiina`, :github-user:`briankung`) - Data truncated for column. :github-issue:`103` (:github-user:`alexhayes`) - Fixed ``has_changed`` not working. :github-issue:`95` (:github-user:`spookylukey`) - Fixed proxy model with ``MoneyField`` returns wrong class. :github-issue:`80` (:github-user:`spookylukey`) `0.5.0`_ - 2014-12-15 --------------------- **Added** - Django 1.7 compatibility. (:github-user:`w00kie`) **Fixed** - Added ``choices=`` to instantiation of currency widget. (:github-user:`davidstockwell`) - Nullable ``MoneyField`` should act as ``default=None``. (:github-user:`jakewins`) - Fixed bug where a non-required ``MoneyField`` threw an exception. (:github-user:`spookylukey`) `0.4.2`_ - 2014-07-31 --------------------- `0.4.1`_ - 2013-11-28 --------------------- `0.4.0.0`_ - 2013-11-26 ----------------------- **Added** - Python 3 compatibility. - tox tests. - Format localization. - Template tag ``money_localize``. `0.3.4`_ - 2013-11-25 --------------------- `0.3.3.2`_ - 2013-10-31 ----------------------- `0.3.3.1`_ - 2013-10-01 ----------------------- `0.3.3`_ - 2013-02-17 --------------------- **Added** - South support via implementing the ``south_triple_field`` method. (:github-user:`mattions`) **Fixed** - Fixed issues with money widget not passing attrs up to django's render method, caused id attribute to not be set in html for widgets. (:github-user:`adambregenzer`) - Fixed issue of default currency not being passed on to widget. (:github-user:`snbuchholz`) - Return the right default for South. (:github-user:`mattions`) - Django 1.5 compatibility. (:github-user:`devlocal`) `0.3.2`_ - 2012-11-30 --------------------- **Fixed** - Fixed issues with ``display_for_field`` not detecting fields correctly. (:github-user:`adambregenzer`) - Added South ignore rule to avoid duplicate currency field when using the frozen ORM. (:github-user:`rach`) - Disallow override of objects manager if not setting it up with an instance. (:github-user:`rach`) `0.3.1`_ - 2012-10-11 --------------------- **Fixed** - Fix ``AttributeError`` when Model inherit a manager. (:github-user:`rach`) - Correctly serialize the field. (:github-user:`akumria`) `0.3`_ - 2012-09-30 ------------------- **Added** - Allow django-money to be specified as read-only in a model. (:github-user:`akumria`) - South support: Declare default attribute values. (:github-user:`pjdelport`) `0.2`_ - 2012-04-10 ------------------- - Initial public release .. _Unreleased: https:///github.com/django-money/django-money/compare/3.6.1...HEAD .. _3.6.1: https:///github.com/django-money/django-money/compare/3.6...3.6.1 .. _3.6: https:///github.com/django-money/django-money/compare/3.5.4...3.6 .. _3.5.4: https:///github.com/django-money/django-money/compare/3.5.4...3.5.3 .. _3.5.3: https:///github.com/django-money/django-money/compare/3.5.3...3.5.2 .. _3.5.2: https:///github.com/django-money/django-money/compare/3.5.2...3.5.1 .. _3.5.1: https:///github.com/django-money/django-money/compare/3.5.1...3.5 .. _3.5: https:///github.com/django-money/django-money/compare/3.5...3.4.1 .. _3.4.1: https:///github.com/django-money/django-money/compare/3.4...3.4.1 .. _3.4: https:///github.com/django-money/django-money/compare/3.3...3.4 .. _3.3: https:///github.com/django-money/django-money/compare/3.2...3.3 .. _3.2: https:///github.com/django-money/django-money/compare/3.1...3.2 .. _3.1: https:///github.com/django-money/django-money/compare/3.0...3.1 .. _3.0: https:///github.com/django-money/django-money/compare/2.1.1...3.0 .. _2.1.1: https:///github.com/django-money/django-money/compare/2.1...2.1.1 .. _2.1: https:///github.com/django-money/django-money/compare/2.0.3...2.1 .. _2.0.3: https://github.com/django-money/django-money/compare/2.0.2...2.0.3 .. _2.0.2: https://github.com/django-money/django-money/compare/2.0.1...2.0.2 .. _2.0.1: https://github.com/django-money/django-money/compare/2.0...2.0.1 .. _2.0: https://github.com/django-money/django-money/compare/1.3.1...2.0 .. _1.3.1: https://github.com/django-money/django-money/compare/1.3...1.3.1 .. _1.3: https://github.com/django-money/django-money/compare/1.2.2...1.3 .. _1.2.2: https://github.com/django-money/django-money/compare/1.2.1...1.2.2 .. _1.2.1: https://github.com/django-money/django-money/compare/1.2...1.2.1 .. _1.2: https://github.com/django-money/django-money/compare/1.1...1.2 .. _1.1: https://github.com/django-money/django-money/compare/1.0...1.1 .. _1.0: https://github.com/django-money/django-money/compare/0.15.1...1.0 .. _0.15.1: https://github.com/django-money/django-money/compare/0.15.1...0.15 .. _0.15: https://github.com/django-money/django-money/compare/0.15...0.14.4 .. _0.14.4: https://github.com/django-money/django-money/compare/0.14.4...0.14.3 .. _0.14.3: https://github.com/django-money/django-money/compare/0.14.3...0.14.2 .. _0.14.2: https://github.com/django-money/django-money/compare/0.14.2...0.14.1 .. _0.14.1: https://github.com/django-money/django-money/compare/0.14.1...0.14 .. _0.14: https://github.com/django-money/django-money/compare/0.14...0.13.5 .. _0.13.5: https://github.com/django-money/django-money/compare/0.13.4...0.13.5 .. _0.13.4: https://github.com/django-money/django-money/compare/0.13.3...0.13.4 .. _0.13.3: https://github.com/django-money/django-money/compare/0.13.2...0.13.3 .. _0.13.2: https://github.com/django-money/django-money/compare/0.13.1...0.13.2 .. _0.13.1: https://github.com/django-money/django-money/compare/0.13...0.13.1 .. _0.13: https://github.com/django-money/django-money/compare/0.12.3...0.13 .. _0.12.3: https://github.com/django-money/django-money/compare/0.12.2...0.12.3 .. _0.12.2: https://github.com/django-money/django-money/compare/0.12.1...0.12.2 .. _0.12.1: https://github.com/django-money/django-money/compare/0.12...0.12.1 .. _0.12: https://github.com/django-money/django-money/compare/0.11.4...0.12 .. _0.11.4: https://github.com/django-money/django-money/compare/0.11.3...0.11.4 .. _0.11.3: https://github.com/django-money/django-money/compare/0.11.2...0.11.3 .. _0.11.2: https://github.com/django-money/django-money/compare/0.11.1...0.11.2 .. _0.11.1: https://github.com/django-money/django-money/compare/0.11...0.11.1 .. _0.11: https://github.com/django-money/django-money/compare/0.10.2...0.11 .. _0.10.2: https://github.com/django-money/django-money/compare/0.10.1...0.10.2 .. _0.10.1: https://github.com/django-money/django-money/compare/0.10...0.10.1 .. _0.10: https://github.com/django-money/django-money/compare/0.9.1...0.10 .. _0.9.1: https://github.com/django-money/django-money/compare/0.9.0...0.9.1 .. _0.9.0: https://github.com/django-money/django-money/compare/0.8...0.9.0 .. _0.8: https://github.com/django-money/django-money/compare/0.7.6...0.8 .. _0.7.6: https://github.com/django-money/django-money/compare/0.7.5...0.7.6 .. _0.7.5: https://github.com/django-money/django-money/compare/0.7.4...0.7.5 .. _0.7.4: https://github.com/django-money/django-money/compare/0.7.3...0.7.4 .. _0.7.3: https://github.com/django-money/django-money/compare/0.7.2...0.7.3 .. _0.7.2: https://github.com/django-money/django-money/compare/0.7.1...0.7.2 .. _0.7.1: https://github.com/django-money/django-money/compare/0.7.0...0.7.1 .. _0.7.0: https://github.com/django-money/django-money/compare/0.6.0...0.7.0 .. _0.6.0: https://github.com/django-money/django-money/compare/0.5.0...0.6.0 .. _0.5.0: https://github.com/django-money/django-money/compare/0.4.2...0.5.0 .. _0.4.2: https://github.com/django-money/django-money/compare/0.4.1...0.4.2 .. _0.4.1: https://github.com/django-money/django-money/compare/0.4.0.0...0.4.1 .. _0.4.0.0: https://github.com/django-money/django-money/compare/0.3.4...0.4.0.0 .. _0.3.4: https://github.com/django-money/django-money/compare/0.3.3.2...0.3.4 .. _0.3.3.2: https://github.com/django-money/django-money/compare/0.3.3.1...0.3.3.2 .. _0.3.3.1: https://github.com/django-money/django-money/compare/0.3.3...0.3.3.1 .. _0.3.3: https://github.com/django-money/django-money/compare/0.3.2...0.3.3 .. _0.3.2: https://github.com/django-money/django-money/compare/0.3.1...0.3.2 .. _0.3.1: https://github.com/django-money/django-money/compare/0.3...0.3.1 .. _0.3: https://github.com/django-money/django-money/compare/0.2...0.3 .. _0.2: https://github.com/django-money/django-money/compare/0.2...a6d90348085332a393abb40b86b5dd9505489b04 django-money/docs/conf.py000066400000000000000000000227031521124646200157030ustar00rootroot00000000000000# # Django-money documentation build configuration file, created by # sphinx-quickstart on Thu Sep 29 11:06:12 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) import sphinx_rtd_theme from djmoney import __version__ extlinks = { "github-issue": ("https://github.com/django-money/django-money/issues/%s", "#%s"), "github-user": ("https://github.com/%s", "@%s:github.com"), } # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.extlinks", 'sphinx.ext.viewcode', 'sphinx.ext.autosectionlabel', ] # Make sure the target is unique autosectionlabel_prefix_document = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Django-money' copyright = '2016, Jacob Hansson' author = 'Jacob Hansson' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = release = __version__ # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Django-moneydoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Django-money.tex', 'Django-money Documentation', 'Jacob Hansson', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'django-money', 'Django-money Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Django-money', 'Django-money Documentation', author, 'Django-money', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False suppress_warnings = ['image.nonlocal_uri'] django-money/docs/contributing.rst000066400000000000000000000000411521124646200176340ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst django-money/docs/index.rst000066400000000000000000000004441521124646200162430ustar00rootroot00000000000000Welcome to Django-money's documentation! ======================================== .. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 2 changes contributing settings Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-money/docs/make.bat000066400000000000000000000155261521124646200160160ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-money.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-money.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end django-money/docs/settings.rst000066400000000000000000000033441521124646200167760ustar00rootroot00000000000000Settings ======== The following settings are available in `djmoney/settings.py `_ - documentation pull requests are welcome. * DEFAULT_CURRENCY (documented below) * CURRENCIES (undocumented) * CURRENCY_CHOICES (undocumented) * CURRENCY_DECIMAL_PLACES (undocumented) * CURRENCY_CODE_MAX_LENGTH (undocumented) * MONEY_FORMAT (undocumented) Settings for currency conversion: * OPEN_EXCHANGE_RATES_URL (undocumented) * OPEN_EXCHANGE_RATES_APP_ID (undocumented) * FIXER_URL (undocumented) * FIXER_ACCESS_KEY (undocumented) * UNIRATE_URL (undocumented) * UNIRATE_ACCESS_KEY (undocumented) * BASE_CURRENCY (undocumented) * EXCHANGE_BACKEND (undocumented) * RATES_CACHE_TIMEOUT (undocumented) DEFAULT_CURRENCY ----------------- The `DEFAULT_CURRENCY` setting specifies the default currency used for monetary fields in your Django application. By default, it is set to `None`, meaning no default currency is applied unless explicitly configured. ### Configuration You can configure the `DEFAULT_CURRENCY` in your Django project's `settings.py` file. For example: .. code-block:: python DEFAULT_CURRENCY = "USD" This will set the default currency to US Dollars. ### Usage in Fields The `DEFAULT_CURRENCY` is used in fields like `MoneyField` to set a default currency. If not explicitly overridden, the field will use the globally configured `DEFAULT_CURRENCY`. Example: .. code-block:: python from djmoney.models.fields import MoneyField class Product(models.Model): price = MoneyField(max_digits=10, decimal_places=2, default_currency="EUR") In this example, the `price` field will use Euros (`EUR`) as its currency, overriding the global `DEFAULT_CURRENCY`. django-money/mypy.ini000066400000000000000000000003421521124646200151460ustar00rootroot00000000000000[mypy] python_version = 3.8 ignore_missing_imports = True plugins = mypy_django_plugin.main exclude = (?x)( docs/.+ | .github/.+ ) [mypy.plugins.django-stubs] django_settings_module = "djmoney.settings" django-money/pyproject.toml000066400000000000000000000042571521124646200163740ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 75.3.2", "wheel"] build-backend = "setuptools.build_meta" [project] name = "django-money" dynamic = ["version"] description = "Adds support for using money and currency fields in django models and forms. Uses py-moneyed as the money implementation." readme = "README.rst" license = { text = "BSD" } requires-python = ">=3.10" maintainers = [ { name = "Greg Reinbach", email = "greg@reinbach.com" }, { name = "Benjamin Balder Bach", email = "benjamin@overtag.dk" }, { name = "Dmitry Dygalo", email = "dmitry@dygalo.dev" }, ] keywords = [ "django", "money", "py-money", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "Django>=4.2", "py-moneyed>=2.0,<3.1", ] [project.optional-dependencies] exchange = [ "certifi", ] [dependency-groups] test = [ "django-stubs", "mixer", "mypy", "pytest-cov", "pytest-django", "pytest>=8.2,<8.3", ] docs = [ "Sphinx>=6,<7", "sphinx_rtd_theme>=1.2,<1.3", ] [project.urls] Homepage = "https://github.com/django-money/django-money" [tool.setuptools.dynamic] version = { attr = "djmoney.__version__" } [tool.black] line-length = 120 target-version = ["py38"] [tool.isort] profile = "black" line_length = 120 combine_as_imports = true known_django = "django" known_first_party = ["tests", "djmoney", "moneyed"] known_third_party = ["moneyed", "pytest", "rest_framework", "sphinx_rtd_theme"] sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] lines_after_imports = 2 django-money/pytest.ini000066400000000000000000000003521521124646200155010ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE=tests.settings django_find_project = false filterwarnings = error::DeprecationWarning ignore:pkg_resources is deprecated as an API:DeprecationWarning addopts = "--no-cov-on-fail" pythonpath = . django-money/setup.cfg000066400000000000000000000002771521124646200152770ustar00rootroot00000000000000[flake8] ignore = E226,E302,E41,W503 max-line-length = 120 max-complexity = 10 exclude = migrations [pep8] ignore = E226,E302,E41,E402 max-line-length = 120 exclude = kolibri/*/migrations/* django-money/tests/000077500000000000000000000000001521124646200146125ustar00rootroot00000000000000django-money/tests/__init__.py000066400000000000000000000000001521124646200167110ustar00rootroot00000000000000django-money/tests/conftest.py000066400000000000000000000014441521124646200170140ustar00rootroot00000000000000import pytest from djmoney.contrib.exchange.models import ExchangeBackend, Rate, get_default_backend_name from djmoney.money import Money from tests.testapp.models import InheritorModel, ModelWithDefaultAsInt @pytest.fixture def m2m_object(): return ModelWithDefaultAsInt.objects.create(money=Money(100, "USD")) @pytest.fixture def backend(): return ExchangeBackend.objects.create(name=get_default_backend_name(), base_currency="USD") @pytest.fixture def autoconversion(backend, settings): settings.AUTO_CONVERT_MONEY = True Rate.objects.create(currency="EUR", value="0.88", backend=backend) @pytest.fixture def concrete_instance(m2m_object): instance = InheritorModel.objects.create() instance.m2m_field.add(m2m_object) return instance pytest_plugins = "pytester" django-money/tests/contrib/000077500000000000000000000000001521124646200162525ustar00rootroot00000000000000django-money/tests/contrib/__init__.py000066400000000000000000000000001521124646200203510ustar00rootroot00000000000000django-money/tests/contrib/exchange/000077500000000000000000000000001521124646200200345ustar00rootroot00000000000000django-money/tests/contrib/exchange/__init__.py000066400000000000000000000000001521124646200221330ustar00rootroot00000000000000django-money/tests/contrib/exchange/conftest.py000066400000000000000000000057241521124646200222430ustar00rootroot00000000000000import json from contextlib import contextmanager from decimal import Decimal from unittest.mock import Mock, patch from django.core.cache import cache import pytest from djmoney.contrib.exchange.backends import FixerBackend, OpenExchangeRatesBackend, UniRateBackend from djmoney.contrib.exchange.backends.base import BaseExchangeBackend from djmoney.contrib.exchange.models import ExchangeBackend, Rate pytestmarks = pytest.mark.django_db OPEN_EXCHANGE_RATES_RESPONSE = """{ "disclaimer": "Usage subject to terms: https://openexchangerates.org/terms", "license": "https://openexchangerates.org/license", "timestamp": 1522749600, "base": "USD", "rates": {"EUR": 0.812511, "NOK": 7.847505, "SEK": 8.379037} }""" OPEN_EXCHANGE_RATES_EXPECTED = json.loads(OPEN_EXCHANGE_RATES_RESPONSE, parse_float=Decimal)["rates"] FIXER_RESPONSE = """{ "success":true, "timestamp":1522788248, "base":"EUR", "date":"2018-04-03", "rates":{"USD":1.227439,"NOK":9.624334,"SEK":10.300293} }""" FIXER_EXPECTED = json.loads(FIXER_RESPONSE, parse_float=Decimal)["rates"] UNIRATE_RESPONSE = """{ "amount": 1.0, "base": "USD", "rates": {"EUR": 0.85, "NOK": 10.5, "SEK": 11.0} }""" UNIRATE_EXPECTED = json.loads(UNIRATE_RESPONSE, parse_float=Decimal)["rates"] @contextmanager def mock_backend(value): response = Mock() response.read.return_value = value with patch("djmoney.contrib.exchange.backends.base.urlopen", return_value=response): yield class ExchangeTest: @pytest.fixture( autouse=True, params=( (OpenExchangeRatesBackend, OPEN_EXCHANGE_RATES_RESPONSE, OPEN_EXCHANGE_RATES_EXPECTED), (FixerBackend, FIXER_RESPONSE, FIXER_EXPECTED), (UniRateBackend, UNIRATE_RESPONSE, UNIRATE_EXPECTED), ), ) def setup(self, request): klass, response_value, expected = request.param self.backend = klass() self.expected = expected with mock_backend(response_value): yield def assert_rates(self): backend = ExchangeBackend.objects.get() assert Rate.objects.count() == len(self.expected) for currency, rate in self.expected.items(): assert Rate.objects.filter(currency=currency, value=rate, backend=backend) class FixedOneBackend(BaseExchangeBackend): name = "first" def get_rates(self, **params): return {"EUR": 1} class FixedTwoBackend(BaseExchangeBackend): name = "second" def get_rates(self, **params): return {"EUR": 2} @pytest.fixture def two_backends_data(): FixedOneBackend().update_rates() FixedTwoBackend().update_rates() @pytest.fixture def simple_rates(backend): Rate.objects.create(currency="EUR", value=2, backend=backend) @pytest.fixture def default_openexchange_rates(): with mock_backend(OPEN_EXCHANGE_RATES_RESPONSE): OpenExchangeRatesBackend().update_rates() @pytest.fixture(autouse=True) def django_cache(): cache.clear() yield django-money/tests/contrib/exchange/test_admin.py000066400000000000000000000005461521124646200225420ustar00rootroot00000000000000from django.contrib.admin import site import pytest from djmoney.contrib.exchange.admin import RateAdmin from djmoney.contrib.exchange.models import Rate pytestmark = pytest.mark.django_db def test_last_update(backend): rate = Rate(currency="NOK", value=5, backend=backend) assert RateAdmin(Rate, site).last_update(rate) == backend.last_update django-money/tests/contrib/exchange/test_backends.py000066400000000000000000000041271521124646200232230ustar00rootroot00000000000000import sys from django.core.exceptions import ImproperlyConfigured import pytest from djmoney.contrib.exchange.backends import FixerBackend, OpenExchangeRatesBackend, UniRateBackend from djmoney.contrib.exchange.models import ExchangeBackend, Rate, get_rate from .conftest import ExchangeTest, FixedOneBackend, FixedTwoBackend pytestmark = pytest.mark.django_db class TestBackends(ExchangeTest): def test_get_rates(self): assert self.backend.get_rates() == self.expected def test_initial_update_rates(self): self.backend.update_rates() self.assert_rates() def test_second_update_rates(self): self.backend.update_rates() backend = ExchangeBackend.objects.get(name=self.backend.name) last_update = backend.last_update self.backend.update_rates() backend.refresh_from_db() assert last_update < backend.last_update @pytest.mark.parametrize("backend", (FixerBackend, OpenExchangeRatesBackend, UniRateBackend)) def test_missing_settings(backend): with pytest.raises(ImproperlyConfigured): backend(access_key=None) @pytest.mark.usefixtures("two_backends_data") def test_two_backends(): """ Two different backends should not interfere with each other. """ for backend in (FixedOneBackend, FixedTwoBackend): assert Rate.objects.filter(backend__name=backend.name).count() == 1 assert get_rate("USD", "EUR", backend=FixedOneBackend.name) == 1 assert get_rate("USD", "EUR", backend=FixedTwoBackend.name) == 2 @pytest.fixture def no_certifi(): certifi = sys.modules["certifi"] sys.modules["certifi"] = None base = sys.modules.pop("djmoney.contrib.exchange.backends.base") yield sys.modules["certifi"] = certifi # Return the old module to keep all existing mocks in other tests sys.modules["djmoney.contrib.exchange.backends.base"] = base @pytest.mark.usefixtures("no_certifi") def test_certifi_not_installed(): with pytest.raises(ImportError, match="Please install dependency certifi - pip install certifi"): __import__("djmoney.contrib.exchange.backends.base") django-money/tests/contrib/exchange/test_commands.py000066400000000000000000000017671521124646200232610ustar00rootroot00000000000000from django.core.management import call_command import pytest from djmoney.contrib.exchange.models import Rate from .conftest import ExchangeTest, FixedOneBackend, FixedTwoBackend pytestmark = pytest.mark.django_db BACKEND_PATH = FixedOneBackend.__module__ + "." + FixedOneBackend.__name__ class TestCommand(ExchangeTest): def test_update_rates(self): call_command("update_rates") self.assert_rates() def test_custom_backend(): call_command("update_rates", backend=BACKEND_PATH) assert Rate.objects.filter(currency="EUR", value=1).exists() @pytest.mark.usefixtures("two_backends_data") class TestClearRates: def test_for_specific_backend(self): call_command("clear_rates", backend=BACKEND_PATH) assert not Rate.objects.filter(backend=FixedOneBackend.name).exists() assert Rate.objects.filter(backend=FixedTwoBackend.name).exists() def test_for_all(self): call_command("clear_rates", all=True) assert not Rate.objects.exists() django-money/tests/contrib/exchange/test_model.py000066400000000000000000000066001521124646200225470ustar00rootroot00000000000000from decimal import Decimal from textwrap import dedent from unittest.mock import patch from django.core.exceptions import ImproperlyConfigured import pytest from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.models import _get_rate, convert_money, get_rate from djmoney.money import Currency, Money pytestmark = pytest.mark.django_db @pytest.mark.parametrize( "source, target, expected, queries", ( ("USD", "USD", 1, 0), ("USD", "EUR", 2, 1), ("EUR", "USD", Decimal("0.5"), 1), (Currency("USD"), "USD", 1, 0), ("USD", Currency("USD"), 1, 0), ), ) @pytest.mark.usefixtures("simple_rates") def test_get_rate(source, target, expected, django_assert_num_queries, queries): with django_assert_num_queries(queries): assert get_rate(source, target) == expected @pytest.mark.parametrize( "source, target, expected", ( ("NOK", "SEK", Decimal("1.067732610555839085161462146")), ("SEK", "NOK", Decimal("0.9365640705489186883886537319")), ), ) @pytest.mark.usefixtures("default_openexchange_rates") def test_rates_via_base(source, target, expected, django_assert_num_queries): with django_assert_num_queries(1): assert get_rate(source, target) == expected @pytest.mark.parametrize("source, target", (("NOK", "ZAR"), ("ZAR", "NOK"), ("USD", "ZAR"), ("ZAR", "USD"))) @pytest.mark.usefixtures("default_openexchange_rates") def test_unknown_currency_with_partially_exiting_currencies(source, target): with pytest.raises(MissingRate, match=f"Rate {source} \\-\\> {target} does not exist"): get_rate(source, target) @pytest.mark.parametrize("source, target", (("USD", "EUR"), ("SEK", "ZWL"))) def test_unknown_currency(source, target): with pytest.raises(MissingRate, match=f"Rate {source} \\-\\> {target} does not exist"): get_rate(source, target) def test_string_representation(backend): assert str(backend) == backend.name @pytest.mark.usefixtures("simple_rates") def test_cache(): with patch("djmoney.contrib.exchange.models._get_rate", wraps=_get_rate) as original: assert get_rate("USD", "USD") == 1 assert original.call_count == 0 assert get_rate("USD", "EUR") == 2 assert original.call_count == 1 assert get_rate("USD", "EUR") == 2 assert original.call_count == 1 def test_bad_configuration(settings): settings.INSTALLED_APPS.remove("djmoney.contrib.exchange") with pytest.raises(ImproperlyConfigured): convert_money(Money(1, "USD"), "EUR") settings.INSTALLED_APPS.append("djmoney.contrib.exchange") def test_without_installed_exchange(testdir): """ If there is no 'djmoney.contrib.exchange' in INSTALLED_APPS importing `Money` should not cause a RuntimeError. Details: GH-385. """ testdir.mkpydir("money_app") testdir.makepyfile( app_settings=""" DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'test.db', } } INSTALLED_APPS = ['djmoney'] SECRET_KEY = 'foobar' """ ) result = testdir.runpython_c( dedent( """ import os os.environ['DJANGO_SETTINGS_MODULE'] = 'app_settings' from django import setup setup() from djmoney.money import Money print(Money(1, 'USD')) """ ) ) result.stdout.fnmatch_lines(["$1.00"]) django-money/tests/contrib/test_django_rest_framework.py000066400000000000000000000351121521124646200242410ustar00rootroot00000000000000from collections import Counter from decimal import Decimal from django.test import override_settings import pytest from djmoney.money import Money from ..testapp.models import ( ModelWithVanillaMoneyField, MoneyFieldModelWithProperty, NullMoneyFieldModel, ValidatedMoneyModel, ) pytestmark = pytest.mark.django_db serializers = pytest.importorskip("rest_framework.serializers") fields = pytest.importorskip("rest_framework.fields") djmoney_fields = pytest.importorskip("djmoney.contrib.django_rest_framework.fields") class TestMoneyField: def get_serializer( self, model_class, field_name=None, instance=None, data=fields.empty, fields_="__all__", field_kwargs=None ): class MetaSerializer(serializers.SerializerMetaclass): def __new__(cls, name, bases, attrs): from djmoney.contrib.django_rest_framework import MoneyField if field_name is not None and field_kwargs is not None: attrs[field_name] = MoneyField(max_digits=10, decimal_places=2, **field_kwargs) return super().__new__(cls, name, bases, attrs) class Serializer(serializers.ModelSerializer, metaclass=MetaSerializer): class Meta: model = model_class fields = fields_ return Serializer(instance=instance, data=data) @pytest.mark.parametrize( "model_class, create_kwargs, expected", ( (NullMoneyFieldModel, {"field": None}, {"field": None, "field_currency": "USD"}), (NullMoneyFieldModel, {"field": Money(10, "USD")}, {"field": "10.00", "field_currency": "USD"}), ( ModelWithVanillaMoneyField, {"money": Money(10, "USD")}, { "integer": 0, "money": "10.00", "money_currency": "USD", "second_money": "0.00", "second_money_currency": "EUR", }, ), ), ) def test_to_representation(self, model_class, create_kwargs, expected): instance = model_class.objects.create(**create_kwargs) expected["id"] = instance.id serializer = self.get_serializer(model_class, instance=instance) assert serializer.data == expected @pytest.mark.parametrize( "model_class, field, field_kwargs, value, expected", ( (NullMoneyFieldModel, "field", None, None, None), (NullMoneyFieldModel, "field", {"default_currency": "EUR", "allow_null": True}, None, None), (NullMoneyFieldModel, "field", None, Money(10, "USD"), Money(10, "USD")), (NullMoneyFieldModel, "field", {"default_currency": "EUR"}, Money(10, "USD"), Money(10, "USD")), (ModelWithVanillaMoneyField, "money", {"default_currency": "EUR"}, 10, Money(10, "EUR")), (ModelWithVanillaMoneyField, "money", None, Money(10, "USD"), Money(10, "USD")), (ModelWithVanillaMoneyField, "money", {"default_currency": "EUR"}, Money(10, "USD"), Money(10, "USD")), (ModelWithVanillaMoneyField, "money", {"default_currency": "EUR"}, 10, Money(10, "EUR")), ), ) def test_to_internal_value(self, model_class, field, field_kwargs, value, expected): serializer = self.get_serializer(model_class, field_name=field, data={field: value}, field_kwargs=field_kwargs) assert serializer.is_valid() instance = serializer.save() assert getattr(instance, field) == expected def test_to_internal_value_for_property_field(self): class PropertyModelSerializer(serializers.ModelSerializer): extra_monies = djmoney_fields.MoneyField( source="ten_extra_monies", max_digits=10, decimal_places=2, min_value=0, default_currency="EUR", required=False, allow_null=True, read_only=True, ) class Meta: model = MoneyFieldModelWithProperty fields = ("extra_monies", "money") serializer = PropertyModelSerializer(data={"money": Money(12, "USD"), "ten_extra_monies": Money(100, "USD")}) serializer.is_valid() assert serializer.validated_data == {"money": Money("12.00", "USD")} instance = serializer.save() assert instance.ten_extra_monies == Money(22, "USD") assert instance.money == Money(12, "USD") def test_invalid_value(self): serializer = self.get_serializer(ModelWithVanillaMoneyField, data={"money": None}) assert not serializer.is_valid() error_text = "This field may not be null." assert serializer.errors == {"money": [error_text]} @pytest.mark.parametrize( "body, field_kwargs, expected", ( ({"field": "10", "field_currency": "EUR"}, None, Money(10, "EUR")), ({"field": "10"}, {"default_currency": "EUR"}, Money(10, "EUR")), ({"field": "12.20", "field_currency": "GBP"}, None, Money(12.20, "GBP")), ({"field": "15.15", "field_currency": "USD"}, None, Money(15.15, "USD")), ({"field": None, "field_currency": None}, None, None), ({"field": None, "field_currency": None}, {"default_currency": "EUR"}, None), ({"field": "16", "field_currency": None}, None, Decimal("16.00")), ({"field": "16", "field_currency": None}, {"default_currency": "EUR"}, Decimal("16.00")), ({"field": None, "field_currency": "USD"}, None, None), ({"field": None, "field_currency": "USD"}, {"default_currency": "EUR"}, None), ), ) def test_post_put_values(self, body, field_kwargs, expected): if field_kwargs is not None: field_kwargs["allow_null"] = True serializer = self.get_serializer(NullMoneyFieldModel, data=body, field_name="field", field_kwargs=field_kwargs) serializer.is_valid() assert serializer.validated_data["field"] == expected def test_serializer_with_fields(self): serializer = self.get_serializer(ModelWithVanillaMoneyField, data={"money": "10.00"}, fields_=("money",)) serializer.is_valid(raise_exception=True) assert serializer.data == {"money": "10.00"} @pytest.mark.parametrize( "value, error", ( (Money(50, "EUR"), "Ensure this value is greater than or equal to €100.00."), (Money(1500, "EUR"), "Ensure this value is less than or equal to €1,000.00."), (Money(40, "USD"), "Ensure this value is greater than or equal to $50.00."), (Money(600, "USD"), "Ensure this value is less than or equal to $500.00."), (Money(400, "NOK"), "Ensure this value is greater than or equal to NOK500.00."), (Money(950, "NOK"), "Ensure this value is less than or equal to NOK900.00."), (Money(10, "DKK"), "Ensure this value is greater than or equal to DKK50.00."), (Money(2500, "DKK"), "Ensure this value is less than or equal to DKK2,000.00."), (Money(5, "SEK"), "Ensure this value is greater than or equal to 10."), (Money(1600, "SEK"), "Ensure this value is less than or equal to 1500."), ), ) def test_model_validators(self, value, error): serializer = self.get_serializer( ValidatedMoneyModel, data={"money": value.amount, "money_currency": value.currency.code} ) assert not serializer.is_valid() assert serializer.errors["money"][0] == error @pytest.mark.parametrize( "value, error", ( (Money(50, "EUR"), "Ensure this value is greater than or equal to 100."), (Money(1500, "EUR"), "Ensure this value is less than or equal to 1000."), ), ) def test_boundary_values(self, value, error): serializer = self.get_serializer( NullMoneyFieldModel, data={"field": value.amount, "field_currency": value.currency.code}, field_name="field", field_kwargs={"min_value": 100, "max_value": 1000}, ) assert not serializer.is_valid() assert serializer.errors["field"][0] == error @pytest.mark.parametrize( ("data", "error_codes"), [ pytest.param( {"money": "", "money_currency": "XUA"}, [("money", "invalid")], id="amount_as_empty_string", ), pytest.param( {"money": None, "money_currency": "XUA"}, [("money", "null")], id="amount_as_none", ), pytest.param( {"money": "v", "money_currency": "XUA"}, [("money", "invalid")], id="amount_as_invalid_decimal", ), pytest.param( {"money": "0.01", "money_currency": "v"}, [("money", "invalid_currency")], id="invalid_currency", ), pytest.param( {"money_currency": "SEK"}, [("money", "required")], id="amount_key_not_in_data", ), ], ) def test_errors_on(self, data, error_codes): class Serializer(serializers.Serializer): money = djmoney_fields.MoneyField(max_digits=9, decimal_places=2) serializer = Serializer(data=data) with pytest.raises(serializers.ValidationError) as err: serializer.is_valid(raise_exception=True) assert Counter([(field, code) for field, codes in err.value.get_codes().items() for code in codes]) == Counter( error_codes ) @pytest.mark.parametrize( ("data", "expected"), [ pytest.param({"money": "0.01", "money_currency": None}, Decimal("0.01"), id="is_none"), pytest.param({"money": "0.01", "money_currency": ""}, Decimal("0.01"), id="is_empty_string"), pytest.param({"money": "0.01"}, Decimal("0.01"), id="key_not_in_data"), ], ) def test_returns_decimal_when_currency(self, data, expected): class Serializer(serializers.Serializer): money = djmoney_fields.MoneyField(max_digits=9, decimal_places=2) serializer = Serializer(data=data) serializer.is_valid(raise_exception=True) assert serializer.validated_data["money"] == expected # Test case contributed for # https://github.com/django-money/django-money/pull/722 class TestMinValueSerializer: @override_settings(DEFAULT_CURRENCY="EUR") @pytest.mark.parametrize( ("data", "is_valid"), [ pytest.param({"money": Money(-1, "EUR")}, False, id="is_invalid_money_value"), pytest.param({"money": Money(1, "EUR")}, True, id="is_valid_money_value"), pytest.param({"money": "-1", "money_currency": "EUR"}, False, id="is_invalid_dict_value"), pytest.param({"money": "0.01", "money_currency": "EUR"}, True, id="is_valid_dict_value"), ], ) def test_serializer_validator_field_without_default_currency(self, data, is_valid): from djmoney.contrib.django_rest_framework import MoneyField class MinValueSerializer(serializers.Serializer): money = MoneyField(decimal_places=2, max_digits=10, min_value=0) class Meta: model = ModelWithVanillaMoneyField serializer = MinValueSerializer(data=data) if is_valid: assert serializer.is_valid() else: assert not serializer.is_valid() assert serializer.errors["money"][0] == "Ensure this value is greater than or equal to 0." @pytest.mark.parametrize( ("data", "is_valid"), [ pytest.param({"second_money": Money(-1, "EUR")}, False, id="is_invalid_money_value"), pytest.param({"second_money": Money(1, "EUR")}, True, id="is_valid_money_value"), pytest.param({"second_money": "-1", "second_money_currency": "EUR"}, False, id="is_invalid_dict_value"), pytest.param({"second_money": "0.01", "second_money_currency": "EUR"}, True, id="is_valid_dict_value"), ], ) def test_serializer_validator_field_with_default_currencey(self, data, is_valid): from djmoney.contrib.django_rest_framework import MoneyField class MinValueSerializer(serializers.Serializer): second_money = MoneyField(decimal_places=2, max_digits=10, min_value=0) class Meta: model = ModelWithVanillaMoneyField serializer = MinValueSerializer(data=data) if is_valid: assert serializer.is_valid() else: assert not serializer.is_valid() assert serializer.errors["second_money"][0] == "Ensure this value is greater than or equal to 0." def test_no_model_serializer(self): from djmoney.contrib.django_rest_framework import MoneyField class NormalSerializer(serializers.Serializer): the_money = MoneyField(decimal_places=2, max_digits=10, min_value=0) class Meta: fields = ("the_money",) serializer = NormalSerializer(data={"the_money": "0.01", "the_money_currency": "EUR"}) assert serializer.is_valid() def test_model_serializer_with_field_source(self): class ModelSerializer(serializers.ModelSerializer): renamed_money_field = djmoney_fields.MoneyField( source="money", max_digits=10, decimal_places=2, min_value=0 ) class Meta: model = ModelWithVanillaMoneyField fields = ("renamed_money_field",) serializer = ModelSerializer(data={"renamed_money_field": "0.01", "renamed_money_field_currency": "EUR"}) assert serializer.is_valid() def test_model_serializer_with_nonexistent_property_raises_error(self): class PropertyModelSerializer(serializers.ModelSerializer): nonexistent_field = djmoney_fields.MoneyField( max_digits=10, decimal_places=2, min_value=0, default_currency="EUR", required=False, allow_null=True, ) class Meta: model = MoneyFieldModelWithProperty fields = ("nonexistent_field", "money") data = {"nonexistent_field": "12.00", "money": "10.00"} serializer = PropertyModelSerializer(data=data) with pytest.raises(ValueError) as ex: serializer.is_valid() expected_error_message = ( "nonexistent_field is neither a db field nor a property on the model MoneyFieldModelWithProperty" ) assert ex.value.args[0] == expected_error_message django-money/tests/migrations/000077500000000000000000000000001521124646200167665ustar00rootroot00000000000000django-money/tests/migrations/__init__.py000066400000000000000000000000001521124646200210650ustar00rootroot00000000000000django-money/tests/migrations/helpers.py000066400000000000000000000014321521124646200210020ustar00rootroot00000000000000""" This module contains various helpers for migrations testing. """ import os MIGRATION_NAME = "test" def makemigrations(): from django.core.management import call_command from django.db.migrations import questioner # We should answer yes for all migrations questioner questions questioner.input = lambda prompt=None: "y" os.system("find . -name \\*.pyc -delete") call_command("makemigrations", "money_app", name=MIGRATION_NAME) def get_migration(name): return __import__(f"money_app.migrations.{name}_{MIGRATION_NAME}", fromlist=["Migration"]).Migration def get_operations(migration_name): return get_migration(migration_name).operations def migrate(): from django.core.management import call_command call_command("migrate", "money_app") django-money/tests/migrations/test_migrations.py000066400000000000000000000255611521124646200225640ustar00rootroot00000000000000import os from textwrap import dedent from django.db import migrations import pytest import tests from djmoney.models.fields import CurrencyField, MoneyField from .helpers import get_operations class TestMigrationFramework: installed_apps = ["djmoney", "money_app"] migration_output = ["*Applying money_app.0001_test... OK*", "*Applying money_app.0002_test... OK*"] @pytest.fixture(autouse=True) def setup(self, testdir): """ Creates application module, helpers and settings file with basic config. """ self.testdir = testdir self.project_root = testdir.mkpydir("money_app") testdir.makepyfile( app_settings=""" DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'test.db', } } INSTALLED_APPS = %s SECRET_KEY = 'foobar' """ % str(self.installed_apps) ) self.project_root.join("migrations/__init__.py").ensure() testdir.syspathinsert() def make_models(self, content): """ Creates models.py file. """ fd = self.project_root.join("models.py") fd.write(dedent(content)) def make_migration(self, callable_default='Money("0.00 USD")', **fields): """ Creates a model with provided fields and creates a migration for it. """ if fields: fields_definition = ";".join("=".join([field, definition]) for field, definition in fields.items()) else: fields_definition = "pass" self.make_models( """ from django.db import models from djmoney.models.fields import MoneyField, Money # A callable default def default1(): return {callable_default} class Model(models.Model): {fields} """.format( fields=fields_definition, callable_default=callable_default ) ) tests_path = os.path.dirname(os.path.dirname(tests.__file__)) return self.run( f""" import sys sys.path.append('{tests_path}') from tests.migrations.helpers import makemigrations makemigrations() """ ) def make_default_migration(self, field="MoneyField(max_digits=10, decimal_places=2, null=True)"): return self.make_migration(field=field) def run(self, content): return self.testdir.runpython_c( dedent( """ import os os.environ['DJANGO_SETTINGS_MODULE'] = 'app_settings' from django import setup setup() """ ) + dedent(content) ) def create_instance(self): self.run( """ from money_app.models import Model from djmoney.money import Money Model.objects.create(field=Money(10, 'USD')) """ ) def migrate(self): tests_path = os.path.dirname(os.path.dirname(tests.__file__)) return self.run( dedent( f""" import sys sys.path.append('{tests_path}') from tests.migrations.helpers import migrate migrate() """ ) ) def assert_migrate(self, output=None): """ Runs migrations and checks if 2 migrations were applied. """ if output is None: output = self.migration_output migration = self.migrate() migration.stdout.fnmatch_lines(output) def test_create_initial(self): migration = self.make_default_migration() print(migration.stdout) # Django 5 output replaced "-" with "+" # Migrations for 'money_app': # money_app/migrations/0001_test.py # + Create model Model migration.stdout.fnmatch_lines(["*Migrations for 'money_app':*", "*0001_test.py*", "*Create model Model*"]) operations = get_operations("0001") assert len(operations) == 1 assert isinstance(operations[0], migrations.CreateModel) fields = sorted(operations[0].fields) assert len(fields) == 3 assert fields[0][0] == "field" assert isinstance(fields[0][1], MoneyField) assert fields[1][0] == "field_currency" assert isinstance(fields[1][1], CurrencyField) migration = self.migrate() migration.stdout.fnmatch_lines(["*Applying money_app.0001_test... OK*"]) def test_add_field(self): self.make_migration() migration = self.make_default_migration() migration.stdout.fnmatch_lines( [ "*Migrations for 'money_app':*", "*0002_test.py*", "*Add field field to model*", "*Add field field_currency to model*", ] ) operations = get_operations("0002") assert len(operations) == 2 assert isinstance(operations[0], migrations.AddField) assert isinstance(operations[0].field, MoneyField) assert isinstance(operations[1], migrations.AddField) assert isinstance(operations[1].field, CurrencyField) self.assert_migrate() def test_alter_field(self): self.make_default_migration() migration = self.make_migration(field="MoneyField(max_digits=15, decimal_places=2, null=True)") migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Alter field field on model*"] ) operations = get_operations("0002") assert len(operations) == 1 assert isinstance(operations[0], migrations.AlterField) assert isinstance(operations[0].field, MoneyField) assert operations[0].field.max_digits == 15 self.assert_migrate() def test_alter_field_callable(self): """Test that changing the return value of a callable does not produce new migrations.""" self.make_default_migration() # Change the field to have a callable default migration = self.make_migration( field="MoneyField(max_digits=15, decimal_places=2, null=True, default=default1)" ) migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Alter field field on model*"] ) # Change the return value of the callable migration = self.make_migration( callable_default='"USD"', field="MoneyField(max_digits=15, decimal_places=2, null=True, default=default1)" ) migration.stdout.fnmatch_lines(["No changes detected in app 'money_app'"]) def test_alter_field_currency_callable(self): """Test that changing the return value of a callable does not produce new migrations.""" self.make_default_migration() # Change the field to have a callable default migration = self.make_migration( field="MoneyField(max_digits=15, decimal_places=2, null=True, default_currency=default1)" ) migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Alter field field on model*"] ) # Change the return value of the callable migration = self.make_migration( callable_default='"USD"', field="MoneyField(max_digits=15, decimal_places=2, null=True, default_currency=default1)", ) migration.stdout.fnmatch_lines(["No changes detected in app 'money_app'"]) def test_alter_field_currency_choices_callable(self): """Test that changing the return value of a callable does not produce new migrations.""" self.make_default_migration() # Change the field to have a callable default migration = self.make_migration( callable_default='[("USD", "USD")]', field="MoneyField(max_digits=15, decimal_places=2, null=True, currency_choices=default1)", ) migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Alter field field on model*"] ) # Change the return value of the callable migration = self.make_migration( callable_default='[("CNY", "CNY")]', field="MoneyField(max_digits=15, decimal_places=2, null=True, currency_choices=default1)", ) migration.stdout.fnmatch_lines(["No changes detected in app 'money_app'"]) def test_remove_field(self): self.make_default_migration() migration = self.make_migration() migration.stdout.fnmatch_lines( [ "*Migrations for 'money_app':*", "*0002_test.py*", "*Remove field field from model*", "*Remove field field_currency from model*", ] ) operations = get_operations("0002") assert len(operations) == 2 assert isinstance(operations[0], migrations.RemoveField) assert operations[0].name == "field" assert isinstance(operations[1], migrations.RemoveField) assert operations[1].name == "field_currency" self.assert_migrate() def test_rename_field(self): self.make_default_migration() self.assert_migrate(["*Applying money_app.0001_test... OK*"]) self.create_instance() migration = self.make_migration(new_field="MoneyField(max_digits=10, decimal_places=2, null=True)") migration.stdout.fnmatch_lines( [ "*Migrations for 'money_app':*", "*0002_test.py*", "*Rename field field on model to new_field*", "*Rename field field_currency on model to new_field_currency*", ] ) self.assert_migrate(["*Applying money_app.0002_test... OK*"]) result = self.run( """ from money_app.models import Model instance = Model.objects.get() print(instance.new_field) """ ) result.stdout.fnmatch_lines(["$10.00"]) def test_migrate_to_moneyfield(self): self.make_default_migration(field="models.DecimalField(max_digits=10, decimal_places=2, null=True)") migration = self.make_migration(field="MoneyField(max_digits=10, decimal_places=2, null=True)") migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Add field field_currency to model*"] ) self.assert_migrate() def test_migrate_from_moneyfield(self): self.make_default_migration() migration = self.make_migration(field="models.DecimalField(max_digits=10, decimal_places=2, null=True)") migration.stdout.fnmatch_lines( ["*Migrations for 'money_app':*", "*0002_test.py*", "*Remove field field_currency from model*"] ) self.assert_migrate() django-money/tests/settings.py000066400000000000000000000030371521124646200170270ustar00rootroot00000000000000import warnings import django import moneyed DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": {"context_processors": ["django.contrib.auth.context_processors.auth"]}, } ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] warnings.simplefilter("ignore", Warning) INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "djmoney", "djmoney.contrib.exchange", "tests.testapp", ] SITE_ID = 1 SECRET_KEY = "foobar" ROOT_URLCONF = "tests.urls" # This now defaults to True and raises RemovedInDjango50Warning if django.VERSION < (4, 0): USE_L10N = True moneyed.add_currency(code="USDT", numeric="000", name="Tether") OPEN_EXCHANGE_RATES_APP_ID = "test" FIXER_ACCESS_KEY = "test" UNIRATE_ACCESS_KEY = "test" DEFAULT_CURRENCY = "USD" # Additional setting for callables with different defaults TEST_DEFAULT_CURRENCY = "EUR" django-money/tests/settings_reversion.py000066400000000000000000000001151521124646200211150ustar00rootroot00000000000000from .settings import * # noqa INSTALLED_APPS.append("reversion") # noqa django-money/tests/test_admin.py000066400000000000000000000061451521124646200173210ustar00rootroot00000000000000import django import django.contrib.admin.utils as admin_utils from django.urls import reverse import pytest from djmoney.money import Money from .testapp.models import ModelWithParentAndCallableFields, ModelWithVanillaMoneyField, ParentModel MONEY_FIELD = ModelWithVanillaMoneyField._meta.get_field("money") INTEGER_FIELD = ModelWithVanillaMoneyField._meta.get_field("integer") @pytest.mark.parametrize( "value, expected", ( (Money(10, "RUB"), "10,00\xa0RUB"), # Issue 232 (Money(1000, "SAR"), "1\xa0000,00\xa0SAR"), # Issue 196 (Money(1000, "PLN"), "1\xa0000,00\xa0PLN"), # Issue 102 (Money("3.33", "EUR"), "3,33\xa0€"), # Issue 90 ), ) def test_display_for_field(settings, value, expected): # This now defaults to True and raises RemovedInDjango50Warning if django.VERSION < (4, 0): settings.USE_L10N = True # This locale has no definitions in py-moneyed, so it will work for localized money representation. settings.LANGUAGE_CODE = "cs" assert admin_utils.display_for_field(value, MONEY_FIELD, "") == expected def test_default_display(): assert admin_utils.display_for_field(10, INTEGER_FIELD, "") == "10" def test_admin_with_formset(admin_user, admin_client): """Test to assure that formsets with MoneyFields behave correctly""" parent = ParentModel.objects.create() obj = ModelWithParentAndCallableFields.objects.create(parent=parent) url = reverse( "admin:testapp_parentmodel_change", kwargs={ "object_id": obj.pk, }, ) response = admin_client.get(url) assert response.status_code == 200 response = admin_client.post( url, { "_save": "Save", "modelwithparentandcallablefields_set-TOTAL_FORMS": 2, "modelwithparentandcallablefields_set-INITIAL_FORMS": 1, "modelwithparentandcallablefields_set-MIN_NUM_FORMS": 0, "modelwithparentandcallablefields_set-MAX_NUM_FORMS": 2, # The first object exists. We sent back updated values. "modelwithparentandcallablefields_set-0-modelwithcallabledefaultanddefaultcurrency_ptr": obj.pk, "modelwithparentandcallablefields_set-0-parent": 1, "modelwithparentandcallablefields_set-0-money_0": 123, "modelwithparentandcallablefields_set-0-money_1": "CHF", # This is a very sensitive field value, so we are also checking that it's actually part of the generated # form data. "initial-modelwithparentandcallablefields_set-0-money": "0.00 EUR", # We want this to be all default values and check that it is NOT created "modelwithparentandcallablefields_set-1-modelwithcallabledefaultanddefaultcurrency_ptr": "", "modelwithparentandcallablefields_set-1-parent": 1, "modelwithparentandcallablefields_set-1-money_0": "0.00", "modelwithparentandcallablefields_set-1-money_1": "EUR", "initial-modelwithparentandcallablefields_set-1-money": "0.00 EUR", }, ) print(response.content) assert response.status_code == 302 django-money/tests/test_form.py000066400000000000000000000223371521124646200171750ustar00rootroot00000000000000""" Created on May 7, 2011 @author: jake """ import warnings from decimal import Decimal import pytest from djmoney import settings from djmoney.models.fields import MoneyField from djmoney.money import Money from .testapp.forms import ( DefaultMoneyModelForm, DefaultPrecisionModelForm, DisabledFieldForm, MoneyForm, MoneyFormMultipleCurrencies, MoneyModelForm, MoneyModelFormWithCallableCurrencyChoices, MoneyModelFormWithCallableDefault, MoneyModelFormWithCallableDefaultAndCallableDefaultCurrency, MoneyModelFormWithCallableDefaultCurrency, MoneyModelFormWithValidation, NullableModelForm, OptionalMoneyForm, PositiveValidatedMoneyModelForm, PreciseForm, PreciseModelForm, ValidatedMoneyModelForm, ) from .testapp.models import ( ModelWithCallableCurrencyChoices, ModelWithCallableDefault, ModelWithCallableDefaultAndDefaultCurrency, ModelWithCallableDefaultCurrency, ModelWithVanillaMoneyField, NullMoneyFieldModel, ) pytestmark = pytest.mark.django_db def test_save(): money = Money(Decimal("10"), "SEK") form = MoneyModelForm({"money_0": money.amount, "money_1": money.currency}) assert form.is_valid() instance = form.save() retrieved = ModelWithVanillaMoneyField.objects.get(pk=instance.pk) assert money == retrieved.money def test_save_with_callable_default(): """Test if the callable default triggers""" expected_money = Money(Decimal("0"), "EUR") form = MoneyModelFormWithCallableDefault({}) assert form.is_valid() instance = form.save() retrieved = ModelWithCallableDefault.objects.get(pk=instance.pk) assert expected_money == retrieved.money def test_save_with_callable_default_currency(): """Test if the callable default triggers""" expected_money = Money(Decimal("10"), "EUR") form = MoneyModelFormWithCallableDefaultCurrency({"money_0": "10", "money_1": "EUR"}) assert form.fields["money"].initial[0] is None assert form.fields["money"].initial[1] == "EUR" assert """' in form.as_p() def test_fields_default_amount_becomes_forms_initial(): """ Formfield should take field's default amount and put it in form field's initial value """ form = DefaultMoneyModelForm() assert form.fields["money"].initial == [123, "PLN"] def test_no_deprecation_warning(): """ The library's code shouldn't generate any warnings itself. See #262. """ with warnings.catch_warnings(): warnings.simplefilter("error") MoneyField(max_digits=10, decimal_places=2, currency_choices=(("USD", "USD"),)).formfield() class TestValidation: @pytest.mark.parametrize( "value, error", ( (Money(50, "EUR"), "Ensure this value is greater than or equal to €100.00."), (Money(1500, "EUR"), "Ensure this value is less than or equal to €1,000.00."), (Money(40, "USD"), "Ensure this value is greater than or equal to $50.00."), (Money(600, "USD"), "Ensure this value is less than or equal to $500.00."), (Money(400, "NOK"), "Ensure this value is greater than or equal to NOK500.00."), (Money(950, "NOK"), "Ensure this value is less than or equal to NOK900.00."), (Money(5, "SEK"), "Ensure this value is greater than or equal to 10."), (Money(1600, "SEK"), "Ensure this value is less than or equal to 1500."), ), ) def test_invalid(self, value, error): form = ValidatedMoneyModelForm(data={"money_0": value.amount, "money_1": value.currency}) assert not form.is_valid() assert form.errors == {"money": [error]} @pytest.mark.parametrize("value", (Money(150, "EUR"), Money(200, "USD"), Money(50, "SEK"), Money(600, "NOK"))) def test_valid(self, value): assert ValidatedMoneyModelForm(data={"money_0": value.amount, "money_1": value.currency}).is_valid() @pytest.mark.parametrize("value", (Money(-0.01, "EUR"), Money(-1, "USD"), Money(-10, "NOK"), Money(-100, "SEK"))) def test_non_negative_validator(self, value): """Fails if Validator(0) silently allows negative values.""" form = PositiveValidatedMoneyModelForm(data={"money_0": value.amount, "money_1": value.currency}) assert not form.is_valid() assert form.errors == {"money": ["Ensure this value is greater than or equal to 0."]} @pytest.mark.parametrize( "value", (Money(0, "PHP"), Money(0.01, "EUR"), Money(1, "USD"), Money(10, "NOK"), Money(100, "SEK")) ) def test_positive_validator(self, value): """Fails if MinMoneyValidator(0) blocks positive values. MinMoneyValidator(0) should also allow exactly 0. """ form = PositiveValidatedMoneyModelForm(data={"money_0": value.amount, "money_1": value.currency}) assert form.is_valid() def test_default_django_validator(self): form = MoneyModelFormWithValidation(data={"balance_0": 0, "balance_1": "GBP"}) assert not form.is_valid() assert form.errors == {"balance": ["Ensure this value is greater than or equal to £100.00."]} class TestDisabledField: def test_validation(self): instance = ModelWithVanillaMoneyField.objects.create(money=Money("42.00", "USD")) form = DisabledFieldForm(data={}, instance=instance) assert not form.errors assert form.is_valid() def test_has_changed(self): form = DisabledFieldForm(data={}) assert not form.has_changed() @pytest.mark.parametrize("model_class", (PreciseForm, DefaultPrecisionModelForm)) def test_decimal_places_model_form(model_class): """Forms should use DECIMAL_PLACES setting value when none specified.""" expected = str(10**-settings.DECIMAL_PLACES) assert model_class().fields["money"].widget.widgets[0].attrs["step"] == expected def test_precedence_decimal_places_model_form(): """Forms should use decimal_places in field value when specified.""" decimal_places = PreciseModelForm.Meta.model._meta.fields[2].decimal_places expected = str(10**-decimal_places) assert PreciseModelForm().fields["money"].widget.widgets[0].attrs["step"] == expected django-money/tests/test_managers.py000066400000000000000000000160301521124646200200200ustar00rootroot00000000000000from django.db.models import F, Q import pytest from djmoney.models.managers import _expand_money_args, _expand_money_kwargs from djmoney.money import Money from djmoney.utils import get_amount from moneyed import Money as OldMoney from .testapp.models import ModelWithNonMoneyField def assert_leafs(children): assert ("money", Money(0, "USD")) in children assert ("money_currency", "USD") in children class TestExpandMoneyArgs: def test_no_args(self): assert _expand_money_args(ModelWithNonMoneyField(), []) == [] def test_non_q_args(self): assert _expand_money_args(ModelWithNonMoneyField(), ["money"]) == ["money"] def test_exact(self): """ Test (AND: ('money', 0 USD)) results in; (AND: (AND: ('money', 0 USD), ('money_currency', u'USD'))) """ actual = _expand_money_args(ModelWithNonMoneyField(), [Q(money=Money(0, "USD"))]) assert len(actual) == 1 arg = actual[0] assert isinstance(arg, Q) assert arg.connector == Q.AND assert len(arg.children) == 1 assert isinstance(arg.children[0], Q) assert arg.children[0].connector == Q.AND assert_leafs(arg.children[0].children) def test_and(self): """ Test (AND: ('desc', 'foo'), ('money', 0 USD)) results in; (AND: ('desc', 'foo'), (AND: ('money', 0 USD), ('money_currency', u'USD'))) """ actual = _expand_money_args(ModelWithNonMoneyField(), [Q(money=Money(0, "USD"), desc="foo")]) assert len(actual) == 1 arg = actual[0] assert isinstance(arg, Q) assert arg.connector == Q.AND assert len(arg.children) == 2 # Can't guarantee the ordering of children, thus; for child in arg.children: if isinstance(child, tuple): assert ("desc", "foo") == child elif isinstance(child, Q): assert child.connector == Q.AND assert len(child.children) == 2 assert_leafs(child.children) else: pytest.fail("There should only be two elements, a tuple and a Q - not a %s" % child) def test_and_with_or(self): """ Test (OR: (AND: ('desc', 'foo'), ('money', 0 USD)), ('desc', 'bar')) results in: (OR: (AND: ('desc', 'foo'), (AND: ('money', 0 USD), ('money_currency', u'USD'))), ('desc', 'bar')) """ actual = _expand_money_args(ModelWithNonMoneyField(), [Q(money=Money(0, "USD"), desc="foo") | Q(desc="bar")]) assert len(actual) == 1 arg = actual[0] assert isinstance(arg, Q) assert arg.connector == Q.OR assert len(arg.children) == 2 # Can't guarantee the ordering of children, thus; for child in arg.children: if isinstance(child, tuple): assert ("desc", "bar") == child elif isinstance(child, Q): assert len(child.children) == 2 for subchild in child.children: if isinstance(subchild, tuple): assert ("desc", "foo") == subchild elif isinstance(subchild, Q): assert_leafs(subchild.children) else: pytest.fail("There should only be two elements, a tuple and a Q - not a %s" % child) def test_and_with_two_or(self): """ Test (OR: (OR: (AND: ('desc', 'foo'), ('money', 0 USD) ), ('desc', 'eggs') ), ('desc', 'bar') ) results in; (OR: (OR: (AND: ('desc', 'foo'), (AND: ('money', 0), ('money_currency', 'USD') ) ), ('desc', 'eggs') ), ('desc', 'bar') ) """ actual = _expand_money_args( ModelWithNonMoneyField(), [Q(Q(money=Money(0, "USD"), desc="foo") | Q(desc="eggs")) | Q(desc="bar")] ) arg = actual[0] assert len(actual) == 1 assert isinstance(arg, Q) assert arg.connector == Q.OR assert len(arg.children) == 2 # Can't guarantee the ordering of children, thus; for child in arg.children: if isinstance(child, tuple): assert ("desc", "bar") == child elif isinstance(child, Q): assert len(child.children) == 2 for subchild in child.children: if isinstance(subchild, tuple): assert ("desc", "eggs") == subchild elif isinstance(subchild, Q): for subsubchild in subchild.children: if isinstance(subsubchild, tuple): assert ("desc", "foo") == subsubchild elif isinstance(subsubchild, Q): assert_leafs(subsubchild.children) else: pytest.fail( "There should only be two subsubchild elements, a tuple and a Q - not a %s" % subsubchild ) else: pytest.fail( "There should only be two subchild elements, a tuple and a Q - not a %s" % subsubchild ) else: pytest.fail("There should only be two child elements, a tuple and a Q - not a %s" % child) class TestKwargsExpand: @pytest.mark.parametrize( "value, expected", ( ( ({"money": 100, "desc": "test"}, {"money": 100, "desc": "test"}), ({"money": Money(100, "USD")}, {"money": 100, "money_currency": "USD"}), ({"money": OldMoney(100, "USD")}, {"money": 100, "money_currency": "USD"}), ({"money": Money(100, "USD"), "desc": "test"}, {"money": 100, "money_currency": "USD", "desc": "test"}), ) ), ) def test_simple(self, value, expected): assert _expand_money_kwargs(ModelWithNonMoneyField, kwargs=value)[1] == expected @pytest.mark.parametrize( "value, expected", (({"money": F("money") * 2}, 2), ({"money": F("money") + Money(100, "USD")}, 100)) ) def test_complex_f_query(self, value, expected): _, kwargs = _expand_money_kwargs(ModelWithNonMoneyField, kwargs=value) assert isinstance(kwargs["money_currency"], F) assert kwargs["money_currency"].name == "money_currency" assert get_amount(kwargs["money"].rhs) == expected def test_simple_f_query(self): _, kwargs = _expand_money_kwargs(ModelWithNonMoneyField, kwargs={"money": F("money")}) assert isinstance(kwargs["money_currency"], F) assert kwargs["money_currency"].name == "money_currency" assert kwargs["money"].name == "money" django-money/tests/test_models.py000066400000000000000000001173631521124646200175210ustar00rootroot00000000000000""" Created on May 7, 2011 @author: jake """ import datetime from copy import copy from decimal import Decimal, InvalidOperation from django import VERSION from django.core.exceptions import ValidationError from django.db import IntegrityError, models from django.db.migrations.writer import MigrationWriter from django.db.models import Case, F, Func, Q, Value, When from django.db.models.functions import Coalesce from django.utils.translation import override import pytest from djmoney.models.fields import MoneyField from djmoney.money import Money from moneyed import Money as OldMoney from moneyed.classes import CurrencyDoesNotExist from .testapp.models import ( AbstractModel, BaseModel, CryptoModel, DateTimeModel, InheritedModel, InheritorModel, ModelIssue300, ModelRelatedToModelWithMoney, ModelWithCallableCurrencyChoices, ModelWithCallableDefault, ModelWithCallableDefaultAndDefaultCurrency, ModelWithCallableDefaultCurrency, ModelWithChoicesMoneyField, ModelWithCustomDefaultManager, ModelWithCustomManager, ModelWithDefaultAsDecimal, ModelWithDefaultAsFloat, ModelWithDefaultAsInt, ModelWithDefaultAsMoney, ModelWithDefaultAsOldMoney, ModelWithDefaultAsString, ModelWithDefaultAsStringWithCurrency, ModelWithNonMoneyField, ModelWithNullableCurrency, ModelWithNullDefaultOnNonNullableField, ModelWithSharedCurrency, ModelWithTwoMoneyFields, ModelWithUniqueIdAndCurrency, ModelWithVanillaMoneyField, NotNullMoneyFieldModel, NullMoneyFieldModel, ProxyModel, ProxyModelWrapper, SimpleModel, ) pytestmark = pytest.mark.django_db class TestVanillaMoneyField: @pytest.mark.parametrize( "model_class, kwargs, expected", ( (ModelWithVanillaMoneyField, {"money": Money("100.0", "USD")}, Money("100.0", "USD")), (ModelWithVanillaMoneyField, {"money": OldMoney("100.0", "USD")}, Money("100.0", "USD")), (BaseModel, {}, Money(0, "USD")), (BaseModel, {"money": "111.2"}, Money("111.2", "USD")), (BaseModel, {"money": Money("123", "PLN")}, Money("123", "PLN")), (BaseModel, {"money": OldMoney("123", "PLN")}, Money("123", "PLN")), (BaseModel, {"money": ("123", "PLN")}, Money("123", "PLN")), (BaseModel, {"money": (123.0, "PLN")}, Money("123", "PLN")), (ModelWithDefaultAsMoney, {}, Money("0.01", "RUB")), (ModelWithDefaultAsFloat, {}, Money("12.05", "PLN")), (ModelWithDefaultAsStringWithCurrency, {}, Money("123", "USD")), (ModelWithDefaultAsString, {}, Money("123", "PLN")), (ModelWithDefaultAsInt, {}, Money("123", "GHS")), (ModelWithDefaultAsDecimal, {}, Money("0.01", "CHF")), (ModelWithCallableDefault, {}, Money("0.00", "EUR")), # noqa (ModelWithCallableDefaultCurrency, {"money": 0}, Money("0.00", "EUR")), (ModelWithCallableDefaultAndDefaultCurrency, {}, Money("0.00", "EUR")), (ModelWithCallableCurrencyChoices, {"money": Money("0.00", "DKK")}, Money("0.00", "DKK")), (CryptoModel, {"money": Money(10, "USDT")}, Money(10, "USDT")), ), ) def test_create_defaults(self, model_class, kwargs, expected): instance = model_class.objects.create(**kwargs) assert instance.money == expected retrieved = model_class.objects.get(pk=instance.pk) assert retrieved.money == expected def test_old_money_not_mutated_default(self): # See GH-603 money = OldMoney(1, "EUR") # When `moneyed.Money` is passed as a default value to a model class Model(models.Model): price = MoneyField(default=money, max_digits=10, decimal_places=2) class Meta: abstract = True # Its class should remain the same assert type(money) is OldMoney def test_old_money_not_mutated_f_object(self): # See GH-603 money = OldMoney(1, "EUR") instance = ModelWithVanillaMoneyField.objects.create(money=Money(5, "EUR"), integer=2) # When `moneyed.Money` is a part of an F-expression instance.money = F("money") + money # Its class should remain the same assert type(money) is OldMoney def test_old_money_defaults(self): instance = ModelWithDefaultAsOldMoney.objects.create() assert instance.money == Money(".01", "RUB") @pytest.mark.parametrize( "model_class, other_value", ( (BaseModel, Money(0, "USD")), (ModelWithDefaultAsMoney, Money("0.01", "RUB")), (ModelWithDefaultAsFloat, OldMoney("12.05", "PLN")), (ModelWithDefaultAsFloat, Money("12.05", "PLN")), ), ) def test_revert_to_default(self, model_class, other_value): if hasattr(model_class._meta, "get_field"): default_instance = model_class._meta.get_field("money").get_default() else: default_instance = model_class._meta.get_field_by_name("money").default instance1 = model_class.objects.create() pk = instance1.pk # Grab a fresh instance, change the currency to something non-default # and unexpected instance2 = model_class.objects.get(id=pk) instance2.money = Money(other_value.amount, "DKK") instance2.save() instance3 = model_class.objects.get(id=pk) assert instance3.money == Money(other_value.amount, "DKK") # Now change the field back to the default currency instance3.money = copy(default_instance) instance3.save() instance4 = model_class.objects.get(id=pk) assert instance4.money == default_instance @pytest.mark.parametrize("value", ((1, "USD", "extra_string"), (1, None), (1,))) def test_invalid_values(self, value): with pytest.raises(ValidationError): BaseModel.objects.create(money=value) @pytest.mark.parametrize("money_class", (Money, OldMoney)) def test_save_new_value_on_field_without_default(self, money_class): ModelWithVanillaMoneyField.objects.create(money=money_class("100.0", "DKK")) # Try setting the value directly retrieved = ModelWithVanillaMoneyField.objects.get() retrieved.money = Money(1, "DKK") retrieved.save() retrieved = ModelWithVanillaMoneyField.objects.get() assert retrieved.money == Money(1, "DKK") def test_save_new_value_on_field_with_default(self): ModelWithDefaultAsMoney.objects.create() # Try setting the value directly retrieved = ModelWithDefaultAsMoney.objects.get() retrieved.money = Money(1, "DKK") retrieved.save() retrieved = ModelWithDefaultAsMoney.objects.get() assert retrieved.money == Money(1, "DKK") def test_rounding(self): money = Money("100.0623456781123219", "USD") instance = ModelWithVanillaMoneyField.objects.create(money=money) # TODO. Should instance.money be rounded too? retrieved = ModelWithVanillaMoneyField.objects.get(pk=instance.pk) assert retrieved.money == Money("100.06", "USD") @pytest.fixture(params=[Money, OldMoney]) def objects_setup(self, request): Money = request.param ModelWithTwoMoneyFields.objects.bulk_create( ( ModelWithTwoMoneyFields(amount1=Money(1, "USD"), amount2=Money(2, "USD")), ModelWithTwoMoneyFields(amount1=Money(2, "USD"), amount2=Money(0, "USD")), ModelWithTwoMoneyFields(amount1=Money(3, "USD"), amount2=Money(0, "USD")), ModelWithTwoMoneyFields(amount1=Money(4, "USD"), amount2=Money(0, "GHS")), ModelWithTwoMoneyFields(amount1=Money(5, "USD"), amount2=Money(5, "USD")), ModelWithTwoMoneyFields(amount1=Money(5, "EUR"), amount2=Money(5, "USD")), ) ) @pytest.mark.parametrize( "filters, expected_count", ( (Q(amount1=F("amount2")), 1), (Q(amount1__gt=F("amount2")), 2), (Q(amount1__in=(Money(1, "USD"), Money(5, "EUR"))), 2), (Q(id__in=(-1, -2)), 0), (Q(amount1=Money(1, "USD")) | Q(amount2=Money(0, "USD")), 3), (Q(amount1=Money(1, "USD")) | Q(amount1=Money(4, "USD")) | Q(amount2=Money(0, "GHS")), 2), (Q(amount1=OldMoney(1, "USD")) | Q(amount1=OldMoney(4, "USD")) | Q(amount2=OldMoney(0, "GHS")), 2), (Q(amount1=Money(1, "USD")) | Q(amount1=Money(5, "USD")) | Q(amount2=Money(0, "GHS")), 3), (Q(amount1=Money(1, "USD")) | Q(amount1=Money(4, "USD"), amount2=Money(0, "GHS")), 2), (Q(amount1=Money(1, "USD")) | Q(amount1__gt=Money(4, "USD"), amount2=Money(0, "GHS")), 1), (Q(amount1=Money(1, "USD")) | Q(amount1__gte=Money(4, "USD"), amount2=Money(0, "GHS")), 2), ), ) @pytest.mark.usefixtures("objects_setup") def test_comparison_lookup(self, filters, expected_count): assert ModelWithTwoMoneyFields.objects.filter(filters).count() == expected_count def test_date_lookup(self): DateTimeModel.objects.create(field=Money(1, "USD"), created="2016-12-05") assert DateTimeModel.objects.filter(created__date="2016-12-01").count() == 0 assert DateTimeModel.objects.filter(created__date="2016-12-05").count() == 1 @pytest.mark.parametrize( "lookup, rhs, expected", ( ("startswith", 2, 1), ("regex", "^[134]", 3), ("iregex", "^[134]", 3), ("istartswith", 2, 1), ("contains", 5, 2), ("lt", 5, 4), ("endswith", 5, 2), ("iendswith", 5, 2), ("gte", 4, 3), ("iexact", 3, 1), ("exact", 3, 1), ("isnull", True, 0), ("range", (3, 5), 4), ("lte", 2, 2), ("gt", 3, 3), ("icontains", 5, 2), ("in", (1, 0), 1), ), ) @pytest.mark.usefixtures("objects_setup") def test_all_lookups(self, lookup, rhs, expected): kwargs = {"amount1__" + lookup: rhs} assert ModelWithTwoMoneyFields.objects.filter(**kwargs).count() == expected def test_exact_match(self): money = Money("100.0", "USD") instance = ModelWithVanillaMoneyField.objects.create(money=money) retrieved = ModelWithVanillaMoneyField.objects.get(money=money) assert instance.pk == retrieved.pk def test_issue_300_regression(self): date = datetime.datetime(year=2017, month=2, day=1) ModelIssue300.objects.filter(money__created=date) ModelIssue300.objects.filter(money__created__gt=date) def test_range_search(self): money = Money("3", "EUR") instance = ModelWithVanillaMoneyField.objects.create(money=Money("100.0", "EUR")) retrieved = ModelWithVanillaMoneyField.objects.get(money__gt=money) assert instance.pk == retrieved.pk assert ModelWithVanillaMoneyField.objects.filter(money__lt=money).count() == 0 def test_filter_chaining(self): usd_instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD")) ModelWithVanillaMoneyField.objects.create(money=Money(100, "EUR")) query = ModelWithVanillaMoneyField.objects.filter().filter(money=Money(100, "USD")) assert usd_instance in query assert query.count() == 1 @pytest.mark.parametrize("model_class", (ModelWithVanillaMoneyField, ModelWithChoicesMoneyField)) def test_currency_querying(self, model_class): model_class.objects.create(money=Money("100.0", "ZWN")) assert model_class.objects.filter(money__lt=Money("1000", "USD")).count() == 0 assert model_class.objects.filter(money__lt=Money("1000", "ZWN")).count() == 1 @pytest.mark.usefixtures("objects_setup") def test_in_lookup(self): assert ModelWithTwoMoneyFields.objects.filter(amount1__in=(Money(1, "USD"), Money(5, "EUR"))).count() == 2 assert ( ModelWithTwoMoneyFields.objects.filter( Q(amount1__lte=Money(2, "USD")), amount1__in=(Money(1, "USD"), Money(3, "USD")) ).count() == 1 ) assert ModelWithTwoMoneyFields.objects.exclude(amount1__in=(Money(1, "USD"), Money(5, "EUR"))).count() == 4 assert ModelWithTwoMoneyFields.objects.filter(amount1__in=(1, Money(5, "EUR"))).count() == 2 assert ModelWithTwoMoneyFields.objects.filter(amount1__in=(1, 5)).count() == 3 @pytest.mark.usefixtures("objects_setup") def test_in_lookup_f_expression(self): assert ModelWithTwoMoneyFields.objects.filter(amount1__in=(Money(4, "USD"), F("amount2"))).count() == 2 def test_isnull_lookup(self): NullMoneyFieldModel.objects.create(field=None) NullMoneyFieldModel.objects.create(field=Money(100, "USD")) queryset = NullMoneyFieldModel.objects.filter(field=None) assert queryset.count() == 1 def test_null_default(self): instance = NullMoneyFieldModel.objects.create() assert instance.field is None def test_implicit_currency_field_not_nullable_when_money_field_not_nullable(self): instance = ModelWithVanillaMoneyField(money=10, money_currency=None) with pytest.raises(TypeError, match=r"Currency code can't be None"): instance.save() def test_raises_type_error_setting_currency_to_none_on_nullable_currency_field_while_having_amount(self): with pytest.raises(ValueError, match=r"Missing currency value"): NullMoneyFieldModel(field=10, field_currency=None) def test_currency_field_null_switch_not_triggered_from_default_currency(self): # We want a sane default behaviour and simply declaring a `MoneyField(...)` # without any default value args should create non nullable amount and currency # fields assert ModelWithVanillaMoneyField._meta.get_field("money").null is False assert ModelWithVanillaMoneyField._meta.get_field("money_currency").null is False def test_currency_field_nullable_when_money_field_is_nullable(self): assert NullMoneyFieldModel._meta.get_field("field").null is True assert NullMoneyFieldModel._meta.get_field("field_currency").null is True @pytest.mark.parametrize( ("default", "error", "error_match"), [ pytest.param("10", AssertionError, r"Default currency can not be `None`", id="string without currency"), pytest.param(b"10", AssertionError, r"Default currency can not be `None`", id="bytes without currency"), pytest.param(13.37, AssertionError, r"Default currency can not be `None`", id="float"), pytest.param(Decimal(10), AssertionError, r"Default currency can not be `None`", id="decimal"), pytest.param(10, AssertionError, r"Default currency can not be `None`", id="int"), pytest.param("100 ", CurrencyDoesNotExist, r"code ", id="string with trailing space and without currency"), pytest.param(b"100 ", CurrencyDoesNotExist, r"code ", id="bytes with trailing space and without currency"), pytest.param("100 ABC123", CurrencyDoesNotExist, r"code ABC123", id="string with unknown currency code"), pytest.param(b"100 ABC123", CurrencyDoesNotExist, r"code ABC123", id="bytes with unknown currency code"), # TODO: Better error reporting on > 1 white spaces between amount and currency as that could be # quite difficult to spot? pytest.param( "100 SEK", CurrencyDoesNotExist, r"code SEK", id="string with too much value and currency separation" ), pytest.param( b"100 SEK", CurrencyDoesNotExist, r"code SEK", id="bytes with too much value and currency separation" ), pytest.param( " 10 SEK ", InvalidOperation, r"(ConversionSyntax|Invalid literal)", id="string with leading and trailing spaces", ), pytest.param( b" 10 SEK ", InvalidOperation, r"(ConversionSyntax|Invalid literal)", id="bytes with leading and trailing spaces", ), pytest.param( " 10 SEK", InvalidOperation, r"(ConversionSyntax|Invalid literal)", id="string with leading spaces" ), pytest.param( b" 10 SEK", InvalidOperation, r"(ConversionSyntax|Invalid literal)", id="bytes with leading spaces" ), pytest.param("10 SEK ", CurrencyDoesNotExist, r"code SEK ", id="string with trailing spaces"), pytest.param(b"10 SEK ", CurrencyDoesNotExist, r"code SEK ", id="bytes with trailing spaces"), ], ) def test_errors_instantiating_money_field_with_no_default_currency_and_default_as(default, error, error_match): with pytest.raises(error, match=error_match): MoneyField(max_digits=10, decimal_places=2, default=default, default_currency=None) @pytest.mark.parametrize( ("default", "default_currency", "expected"), [ pytest.param("10 SEK", None, Money(10, "SEK"), id="string with currency"), pytest.param(b"10 SEK", None, Money(10, "SEK"), id="bytes with currency"), pytest.param("10", "USD", Money(10, "USD"), id="string without currency and currency default"), pytest.param(b"10", "USD", Money(10, "USD"), id="bytes without currency and currency default"), ], ) def test_can_instantiate_money_field_default_as(default, default_currency, expected): field = MoneyField(max_digits=10, decimal_places=2, default=default, default_currency=default_currency) assert field.default == expected def test_errors_on_default_values_being_none_when_fields_have_not_null_constraint(): instance = ModelWithNullDefaultOnNonNullableField() assert instance.money is None assert instance.money_currency is None with pytest.raises(IntegrityError, match=r"testapp_modelwithnulldefaultonnonnullablefield.money"): instance.save() class TestGetOrCreate: @pytest.mark.parametrize( "model, field_name, kwargs, currency", ( (ModelWithDefaultAsInt, "money", {"money_currency": "PLN"}, "PLN"), (ModelWithVanillaMoneyField, "money", {"money": Money(0, "EUR")}, "EUR"), (ModelWithVanillaMoneyField, "money", {"money": OldMoney(0, "EUR")}, "EUR"), (ModelWithSharedCurrency, "first", {"first": 10, "second": 15, "currency": "CZK"}, "CZK"), ), ) def test_get_or_create_respects_currency(self, model, field_name, kwargs, currency): instance, created = model.objects.get_or_create(**kwargs) field = getattr(instance, field_name) assert str(field.currency) == currency, "currency should be taken into account in get_or_create" def test_get_or_create_respects_defaults(self): value = Money(10, "SEK") instance = ModelWithUniqueIdAndCurrency.objects.create(money=value) instance, created = ModelWithUniqueIdAndCurrency.objects.get_or_create( id=instance.id, money_currency=instance.money_currency ) assert not created assert instance.money == value def test_defaults(self): money = Money(10, "EUR") instance, _ = ModelWithVanillaMoneyField.objects.get_or_create(integer=1, defaults={"money": money}) assert instance.money == money def test_currency_field_lookup(self): value = Money(10, "EUR") ModelWithVanillaMoneyField.objects.create(money=value) instance, created = ModelWithVanillaMoneyField.objects.get_or_create(money_currency__iexact="eur") assert not created assert instance.money == value @pytest.mark.parametrize( "model, create_kwargs, get_kwargs", ( (NullMoneyFieldModel, {"field": Money(100, "USD")}, {"field": 100, "field_currency": "USD"}), (ModelWithSharedCurrency, {"first": 10, "second": 15, "currency": "USD"}, {"first": 10, "currency": "USD"}), ), ) def test_no_default_model(self, model, create_kwargs, get_kwargs): model.objects.create(**create_kwargs) instance, created = model.objects.get_or_create(**get_kwargs) assert not created def test_shared_currency(self): instance, created = ModelWithSharedCurrency.objects.get_or_create(first=10, second=15, currency="USD") assert instance.first == Money(10, "USD") assert instance.second == Money(15, "USD") class TestNullableCurrency: def test_create_nullable(self): instance = ModelWithNullableCurrency.objects.create() assert instance.money is None assert instance.money_currency is None def test_create_default(self): money = Money(100, "SEK") instance = ModelWithNullableCurrency.objects.create(money=money) assert instance.money == money def test_fails_with_null_currency(self): with pytest.raises(ValueError) as exc: ModelWithNullableCurrency.objects.create(money=10) assert str(exc.value) == "Missing currency value" assert not ModelWithNullableCurrency.objects.exists() def test_fails_with_null_currency_decimal(self): with pytest.raises(ValueError) as exc: ModelWithNullableCurrency.objects.create(money=Decimal(10)) assert str(exc.value) == "Missing currency value" assert not ModelWithNullableCurrency.objects.exists() def test_fails_with_nullable_but_no_default(self): match = r"NOT NULL constraint failed: testapp_modelwithtwomoneyfields.amount1" with pytest.raises(IntegrityError, match=match): ModelWithTwoMoneyFields.objects.create() def test_query_not_null(self): money = Money(100, "EUR") ModelWithNullableCurrency.objects.create(money=money) instance = ModelWithNullableCurrency.objects.get() assert instance.money == money def test_query_null(self): ModelWithNullableCurrency.objects.create() instance = ModelWithNullableCurrency.objects.get() assert instance.money is None assert instance.money_currency is None class TestFExpressions: parametrize_f_objects = pytest.mark.parametrize( "f_obj, expected", ( (F("money") + Money(100, "USD"), Money(200, "USD")), (F("money") + OldMoney(100, "USD"), Money(200, "USD")), (Money(100, "USD") + F("money"), Money(200, "USD")), (F("money") - Money(100, "USD"), Money(0, "USD")), (Money(100, "USD") - F("money"), Money(0, "USD")), (F("money") * 2, Money(200, "USD")), (F("money") * F("integer"), Money(200, "USD")), (Money(50, "USD") * F("integer"), Money(100, "USD")), (F("integer") * Money(50, "USD"), Money(100, "USD")), (Money(50, "USD") / F("integer"), Money(25, "USD")), (Money(51, "USD") % F("integer"), Money(1, "USD")), # type: ignore (F("money") / 2, Money(50, "USD")), (F("money") % 98, Money(2, "USD")), (F("money") / F("integer"), Money(50, "USD")), (F("money") + F("money"), Money(200, "USD")), (F("money") - F("money"), Money(0, "USD")), ), ) @parametrize_f_objects def test_save(self, f_obj, expected): instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD"), integer=2) instance.money = f_obj instance.save() instance.refresh_from_db() assert instance.money == expected @parametrize_f_objects def test_f_update(self, f_obj, expected): instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD"), integer=2) ModelWithVanillaMoneyField.objects.update(money=f_obj) instance.refresh_from_db() assert instance.money == expected def test_default_update(self): instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD"), integer=2) second_money = Money(100, "USD") ModelWithVanillaMoneyField.objects.update(second_money=second_money) instance.refresh_from_db() assert instance.second_money == second_money @pytest.mark.parametrize( "create_kwargs, filter_value, in_result", ( ({"money": Money(100, "USD"), "second_money": Money(100, "USD")}, {"money": F("money")}, True), ({"money": Money(100, "USD"), "second_money": Money(100, "USD")}, {"money": F("second_money")}, True), ({"money": Money(100, "USD"), "second_money": Money(100, "EUR")}, {"money": F("second_money")}, False), ({"money": Money(50, "USD"), "second_money": Money(100, "USD")}, {"second_money": F("money") * 2}, True), ( {"money": Money(50, "USD"), "second_money": Money(100, "USD")}, {"second_money": F("money") + Money(50, "USD")}, True, ), ({"money": Money(50, "USD"), "second_money": Money(100, "EUR")}, {"second_money": F("money") * 2}, False), ( {"money": Money(50, "USD"), "second_money": Money(100, "EUR")}, {"second_money": F("money") + Money(50, "USD")}, False, ), ), ) def test_filtration(self, create_kwargs, filter_value, in_result): instance = ModelWithVanillaMoneyField.objects.create(**create_kwargs) assert (instance in ModelWithVanillaMoneyField.objects.filter(**filter_value)) is in_result def test_update_fields_save(self): instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD"), integer=2) instance.money = F("money") + Money(100, "USD") instance.save(update_fields=["money"]) instance.refresh_from_db() assert instance.money == Money(200, "USD") INVALID_EXPRESSIONS = [ F("money") + Money(100, "EUR"), F("money") * F("money"), F("money") / F("money"), F("money") % F("money"), F("money") + F("integer"), F("money") + F("second_money"), F("money") ** F("money"), F("money") ** F("integer"), F("money") ** 2, ] @pytest.mark.parametrize("f_obj", INVALID_EXPRESSIONS) def test_invalid_expressions_access(self, f_obj): instance = ModelWithVanillaMoneyField.objects.create(money=Money(100, "USD")) with pytest.raises(ValidationError): instance.money = f_obj class TestExpressions: @pytest.mark.skipif(VERSION[:2] < (2, 2), reason="skipping tests for Django < 2.2") def test_bulk_update(self): assert ModelWithVanillaMoneyField.objects.filter(integer=0).count() == 0 assert ModelWithVanillaMoneyField.objects.filter(integer=1).count() == 0 ModelWithVanillaMoneyField.objects.create(money=Money(1, "USD"), integer=0) ModelWithVanillaMoneyField.objects.create(money=Money(2, "USD"), integer=1) first_inst = ModelWithVanillaMoneyField.objects.get(integer=0) second_inst = ModelWithVanillaMoneyField.objects.get(integer=1) assert first_inst.money == Money(1, "USD") assert second_inst.money == Money(2, "USD") first_inst.money = Money(3, "RUB") second_inst.money = Money(4, "UAH") second_inst.second_money = Money(5, "BYN") ModelWithVanillaMoneyField.objects.bulk_update( [first_inst, second_inst], ("money", "money_currency", "second_money", "second_money_currency") ) assert ModelWithVanillaMoneyField.objects.get(integer=0).money == Money(3, "RUB") assert ModelWithVanillaMoneyField.objects.get(integer=1).money == Money(4, "UAH") assert ModelWithVanillaMoneyField.objects.get(integer=1).second_money == Money(5, "BYN") def test_conditional_update(self): ModelWithVanillaMoneyField.objects.bulk_create( ( ModelWithVanillaMoneyField(money=Money(1, "USD"), integer=0), ModelWithVanillaMoneyField(money=Money(2, "USD"), integer=1), ) ) ModelWithVanillaMoneyField.objects.update(money=Case(When(integer=0, then=Value(10)), default=Value(0))) assert ModelWithVanillaMoneyField.objects.get(integer=0).money == Money(10, "USD") assert ModelWithVanillaMoneyField.objects.get(integer=1).money == Money(0, "USD") def test_update_with_coalesce(self): ModelWithVanillaMoneyField.objects.create(money=Money(1, "USD"), second_money=Money(2, "USD"), integer=0) ModelWithVanillaMoneyField.objects.update( money=Coalesce(F("second_money"), 0, output_field=models.DecimalField()) ) instance = ModelWithVanillaMoneyField.objects.get() assert instance.money == Money("2", "USD") def test_create_func(self): instance = ModelWithVanillaMoneyField.objects.create( money=Func(Value(-10), function="ABS"), money_currency="USD" ) instance.refresh_from_db() assert instance.money == Money(10, "USD") @pytest.mark.parametrize( "value, expected", ((None, None), (10, Money(10, "USD")), (Money(10, "EUR"), Money(10, "EUR"))) ) def test_value_create(self, value, expected): instance = NullMoneyFieldModel.objects.create(field=Value(value)) instance.refresh_from_db() assert instance.field == expected def test_value_create_invalid(self): with pytest.raises(ValidationError): ModelWithVanillaMoneyField.objects.create(money=Value("string"), money_currency="DKK") def test_expressions_for_non_money_fields(self): instance = ModelWithVanillaMoneyField.objects.create(money=Money(1, "USD"), integer=0) assert ModelWithVanillaMoneyField.objects.get(money=F("integer") + 1) == instance assert ModelWithVanillaMoneyField.objects.get(Q(money=F("integer") + 1)) == instance def test_find_models_related_to_money_models(): moneyModel = ModelWithVanillaMoneyField.objects.create(money=Money("100.0", "ZWN")) ModelRelatedToModelWithMoney.objects.create(moneyModel=moneyModel) ModelRelatedToModelWithMoney.objects.get(moneyModel__money=Money("100.0", "ZWN")) ModelRelatedToModelWithMoney.objects.get(moneyModel__money__lt=Money("1000.0", "ZWN")) def test_allow_expression_nodes_without_money(): """Allow querying on expression nodes that are not Money""" desc = "hundred" ModelWithNonMoneyField.objects.create(money=Money(100.0, "USD"), desc=desc) instance = ModelWithNonMoneyField.objects.filter(desc=F("desc")).get() assert instance.desc == desc def test_base_model(): assert BaseModel.objects.model == BaseModel @pytest.mark.parametrize("model_class", (InheritedModel, InheritorModel)) class TestInheritance: """Test inheritance from a concrete and an abstract models""" def test_model(self, model_class): assert model_class.objects.model == model_class def test_fields(self, model_class): first_value = Money("100.0", "ZWN") second_value = Money("200.0", "USD") instance = model_class.objects.create(money=first_value, second_field=second_value) assert instance.money == first_value assert instance.second_field == second_value class TestManager: def test_manager(self): assert hasattr(SimpleModel, "objects") def test_objects_creation(self): SimpleModel.objects.create(money=Money("100.0", "USD")) assert SimpleModel.objects.count() == 1 class TestProxyModel: def test_instances(self): ProxyModel.objects.create(money=Money("100.0", "USD")) assert isinstance(ProxyModel.objects.get(pk=1), ProxyModel) def test_patching(self): ProxyModel.objects.create(money=Money("100.0", "USD")) # This will fail if ProxyModel.objects doesn't have the patched manager assert ProxyModel.objects.filter(money__gt=Money("50.00", "GBP")).count() == 0 class TestDifferentCurrencies: """Test add/sub operations between different currencies""" def test_add_default(self): with pytest.raises(TypeError): Money(10, "EUR") + Money(1, "USD") def test_sub_default(self): with pytest.raises(TypeError): Money(10, "EUR") - Money(1, "USD") @pytest.mark.usefixtures("autoconversion") def test_add_with_auto_convert(self): assert Money(10, "EUR") + Money(1, "USD") == Money("10.88", "EUR") @pytest.mark.usefixtures("autoconversion") def test_sub_with_auto_convert(self): assert Money(10, "EUR") - Money(1, "USD") == Money("9.12", "EUR") def test_eq(self): assert Money(1, "EUR") == Money(1, "EUR") def test_ne(self): assert Money(1, "EUR") != Money(2, "EUR") def test_ne_currency(self): assert Money(10, "EUR") != Money(10, "USD") INSTANCE_ACCESS_MODELS = [ModelWithNonMoneyField, InheritorModel, InheritedModel, ProxyModel] if VERSION[:2] < (3, 2): # Django 3.2 and later does not support AbstractModel instancing INSTANCE_ACCESS_MODELS.append(AbstractModel) @pytest.mark.parametrize("model_class", INSTANCE_ACCESS_MODELS) def test_manager_instance_access(model_class): with pytest.raises(AttributeError): model_class().objects.all() def test_different_hashes(): money = BaseModel._meta.get_field("money") money_currency = BaseModel._meta.get_field("money_currency") assert hash(money) != hash(money_currency) def test_migration_serialization(): serialized = "djmoney.money.Money(100, 'GBP')" assert MigrationWriter.serialize(Money(100, "GBP")) == (serialized, {"import djmoney.money"}) @pytest.mark.parametrize( "model, manager_name", ((ModelWithVanillaMoneyField, "objects"), (ModelWithCustomDefaultManager, "custom")) ) def test_clear_meta_cache(model, manager_name): """ See issue GH-318. """ model._meta._expire_cache() manager_class = getattr(model, manager_name).__class__ assert manager_class.__module__ + "." + manager_class.__name__ == "djmoney.models.managers.MoneyManager" class TestFieldAttributes: def create_class(self, **field_kwargs): class Model(models.Model): field = MoneyField(**field_kwargs) class Meta: app_label = "test" return Model def test_missing_attributes(self): with pytest.raises(ValueError) as exc: self.create_class(default={}) assert str(exc.value) == "default value must be an instance of Money, is: {}" def test_default_currency(self): klass = self.create_class(default_currency=None, default=Money(10, "EUR"), max_digits=10, decimal_places=2) assert str(klass._meta.fields[2].default_currency) == "EUR" instance = klass() assert instance.field == Money(10, "EUR") class TestCustomManager: def test_method(self): assert ModelWithCustomManager.manager.super_method().count() == 0 def test_package_is_importable(): __import__("djmoney.__init__") def test_hash_uniqueness(): """ All fields of any model should have unique hash. """ hashes = [hash(field) for field in ModelWithVanillaMoneyField._meta.get_fields()] assert len(hashes) == len(set(hashes)) def test_override_decorator(): """ When current locale is changed, Money instances should be represented correctly. """ with override("cs"): assert str(Money(10, "CZK")) == "10,00 Kč" def test_properties_access(): with pytest.raises(TypeError) as exc: ModelWithVanillaMoneyField(money=Money(1, "USD"), bla=1) if VERSION[:2] > (4, 0): assert str(exc.value) == "ModelWithVanillaMoneyField() got unexpected keyword arguments: 'bla'" elif VERSION[:2] > (2, 1): assert str(exc.value) == "ModelWithVanillaMoneyField() got an unexpected keyword argument 'bla'" else: assert str(exc.value) == "ModelWithVanillaMoneyField() got an unexpected keyword argument 'bla'" def parametrize_with_q(**kwargs): return pytest.mark.parametrize("args, kwargs", (((), kwargs), ((Q(**kwargs),), {}))) class TestSharedCurrency: @pytest.fixture def instance(self): return ModelWithSharedCurrency.objects.create(first=10, second=15, currency="USD") def test_attributes(self, instance): assert instance.first == Money(10, "USD") assert instance.second == Money(15, "USD") assert instance.currency == "USD" @parametrize_with_q(first=Money(10, "USD")) def test_filter_by_money_match(self, instance, args, kwargs): assert instance in ModelWithSharedCurrency.objects.filter(*args, **kwargs) @parametrize_with_q(first=Money(10, "EUR")) def test_filter_by_money_no_match(self, instance, args, kwargs): assert instance not in ModelWithSharedCurrency.objects.filter(*args, **kwargs) @parametrize_with_q(first=F("second")) def test_f_query(self, args, kwargs): instance = ModelWithSharedCurrency.objects.create(first=10, second=10, currency="USD") assert instance in ModelWithSharedCurrency.objects.filter(*args, **kwargs) @parametrize_with_q(first__in=[Money(10, "USD"), Money(100, "USD")]) def test_in_lookup(self, instance, args, kwargs): assert instance in ModelWithSharedCurrency.objects.filter(*args, **kwargs) def test_create_with_money(self): value = Money(10, "USD") instance = ModelWithSharedCurrency.objects.create(first=value, second=value) assert instance.first == value assert instance.second == value def test_order_by(): def extract_data(instance): return instance.money, instance.integer ModelWithVanillaMoneyField.objects.create(money=Money(10, "AUD"), integer=2) ModelWithVanillaMoneyField.objects.create(money=Money(10, "AUD"), integer=1) ModelWithVanillaMoneyField.objects.create(money=Money(10, "USD"), integer=3) qs = ModelWithVanillaMoneyField.objects.order_by("integer").filter(money=Money(10, "AUD")) assert list(map(extract_data, qs)) == [(Money(10, "AUD"), 1), (Money(10, "AUD"), 2)] def test_distinct_through_wrapper(): NotNullMoneyFieldModel.objects.create(money=10, money_currency="USD") NotNullMoneyFieldModel.objects.create(money=100, money_currency="USD") NotNullMoneyFieldModel.objects.create(money=10, money_currency="EUR") queryset = ProxyModelWrapper.objects.distinct() assert queryset.count() == 3 def test_mixer_blend(): try: from mixer.backend.django import mixer except AttributeError: pass # mixer doesn't work with pypy else: instance = mixer.blend(ModelWithTwoMoneyFields, amount1_currency="EUR", amount2_currency="USD") assert isinstance(instance.amount1, Money) assert isinstance(instance.amount2, Money) @pytest.mark.parametrize( ("attribute", "build_kwargs", "expected"), [ pytest.param( "default_currency", {"max_digits": 9, "null": True, "default_currency": None}, None, id="default_currency_as_none", ), pytest.param( "default_currency", {"max_digits": 9, "null": True, "default_currency": "SEK"}, "SEK", id="default_currency_as_non_default_not_none", ), pytest.param( "currency_max_length", {"max_digits": 9, "currency_max_length": 4}, 4, id="currency_max_length_as_non_default", ), ], ) def test_deconstruct_includes(attribute, build_kwargs, expected): instance = MoneyField(**build_kwargs) __, ___, args, kwargs = instance.deconstruct() new = MoneyField(*args, **kwargs) assert getattr(new, attribute) == getattr(instance, attribute) assert getattr(new, attribute) == expected class TestQuerySet: @pytest.fixture def instance(self): return ModelWithVanillaMoneyField.objects.create() @pytest.mark.xfail def test_instance_from_queryset_with_only_raises_keyerror(self, instance): """ Demonstrates that MoneyFields lack support for only() querysets https://github.com/django-money/django-money/issues/654 """ assert isinstance(instance.money, Money) assert instance.money == Money("0.0", "XYZ") only_instance = ModelWithVanillaMoneyField.objects.filter(pk=instance.pk).only("money").first() assert only_instance.money == Money("0.0", "XYZ") # fails with `Keyerror: 'money_currency'` django-money/tests/test_money.py000066400000000000000000000075361521124646200173650ustar00rootroot00000000000000from django.utils.translation import override import pytest from djmoney.money import DefaultMoney, Money, get_current_locale def test_repr(): assert repr(Money("10.5", "USD")) == "Money('10.5', 'USD')" def test_legacy_repr(): assert repr(Money("10.5", "USD")) == "Money('10.5', 'USD')" def test_html_safe(): assert Money("10.5", "EUR").__html__() == "€10.50" def test_html_unsafe(): class UnsafeMoney(Money): def __str__(self): return "