Skip to content

Instantly share code, notes, and snippets.

@jurrian
Created July 21, 2023 11:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jurrian/fb455cad65e1aad5a7e5af610133c4c3 to your computer and use it in GitHub Desktop.
Save jurrian/fb455cad65e1aad5a7e5af610133c4c3 to your computer and use it in GitHub Desktop.
CurrencyField for Django to enforce accuracy for monetary amounts
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,
}
)
@jurrian
Copy link
Author

jurrian commented Jul 21, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment