Created
July 21, 2023 11:59
-
-
Save jurrian/fb455cad65e1aad5a7e5af610133c4c3 to your computer and use it in GitHub Desktop.
CurrencyField for Django to enforce accuracy for monetary amounts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CurrencyField(DecimalField): | |
"""Special DecimalField with defaults for currency. | |
Currency should be stored in the database with 4 decimal places so any calculations will not have rounding errors. | |
Formfields will use 4 decimal places to avoid rounding errors. Any other display of currency fields uses 2 decimals. | |
All fields that process currency should use this field, especially when used in calculations. | |
""" | |
display_decimals = 2 # Representation decimals | |
db_decimals = 4 # Database storage decimals | |
max_digits = 10 | |
def __init__(self, *args, max_digits=None, decimal_places=None, **kwargs): | |
"""Some values are hard coded as they are supposed to be the same for all. | |
""" | |
super().__init__(*args, **kwargs) | |
self.decimal_places = type(self).display_decimals | |
self.max_digits = type(self).max_digits | |
def deconstruct(self): | |
"""Used in migrations to know the required fields arguments. | |
""" | |
name, path, args, kwargs = super().deconstruct() | |
del kwargs['decimal_places'], kwargs['max_digits'] | |
return name, path, args, kwargs | |
@cached_property | |
def validators(self): | |
"""Allow inserting 4 decimals in forms, they will be rounded to 2 decimals. | |
""" | |
return super(DecimalField, self).validators + [ | |
validators.DecimalValidator(self.max_digits, self.db_decimals) | |
] | |
def db_type(self, connection): | |
"""Columns in the db should always be created with more decimals than the display decimals. | |
This seems to be the only fair way to do it, however is only compatible with PostgreSQL. | |
""" | |
return f'numeric({self.max_digits}, {self.db_decimals})' | |
def formfield(self, **kwargs): | |
"""Configure form fields for all CurrencyFields. The default form field is a DecimalField. | |
It allows inserting up to 4 decimals in forms, which matches the decimal precision in the database. | |
""" | |
return super(DecimalField, self).formfield( | |
**{ | |
"max_digits": self.max_digits, | |
"decimal_places": self.db_decimals, | |
"form_class": forms.DecimalField, | |
**kwargs, | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Featured in this blog article: https://deepintodjango.com/keeping-accurate-amounts-in-django-with-currencyfield