import decimal
from . import babel_precision_provider, numbers
DEFAULT_PRECISION = decimal.Decimal("0.01")
[docs]
class Money(numbers.UnittedDecimal):
"""
Money value with currency.
The pure decimal value is available from the base classes
`~shuup.utils.numbers.UnittedDecimal.value` property (preferred way)
or by casting to `Decimal`.
Money objects with different currencies cannot be compared or
calculated with and will raise `~shuup.utils.numbers.UnitMixupError`.
See `__new__`.
"""
def __new__(cls, value="0", currency=None, *args, **kwargs):
"""
Create new Money instance with given value and currency.
If no currency is given explicitly and `value` has a property
named `currency`, then that will be used. Otherwise currency is
a required argument and not passing one will raise a TypeError.
:param str|numbers.Number value:
Value as string or number.
:param str|None currency:
Currency as ISO-4217 code (3-letter string) or None.
"""
if currency is None and hasattr(value, "currency"):
currency = value.currency
if not currency:
raise TypeError(f"Error! {cls.__name__}: currency must be given.")
instance = super().__new__(cls, value, *args, **kwargs)
instance.currency = currency
return instance
def __repr__(self):
cls_name = type(self).__name__
return f"{cls_name}('{self.value}', {self.currency!r})"
def __reduce_ex__(self, protocol):
return (type(self), (self.value, self.currency))
def __str__(self):
return f"{self.value} {self.currency}"
[docs]
@classmethod
def from_data(cls, value, currency):
return cls(value, currency)
[docs]
def unit_matches_with(self, other):
return self.currency == getattr(other, "currency", None)
[docs]
def new(self, value):
return type(self)(value, currency=self.currency)
[docs]
def as_rounded(self, digits=None, rounding=decimal.ROUND_HALF_EVEN):
"""
Get rounded value of self.
Return the value rounded to given `digits` if specified,
otherwise to the precision of ``self.currency`` as returned by
the precision provider, see `set_precision_provider`.
:type digits: int|None
:param digits: Number of digits to round to or None.
:type rounding: str
:param rounding: Rounding mode to use.
:rtype: Money
:return: A new `Money` instance with the rounded value.
"""
precision = _get_precision(self.currency, digits)
return self.new(self.value.quantize(precision, rounding=rounding))
[docs]
def set_precision_provider(precision_provider):
"""
Set precision provider for Money instances.
Default precision provider is
`shuup.utils.babel_precision_provider.get_precision`.
:type precision_provider: Callable[[str], decimal.Decimal|None]
:param precision_provider:
Function which will return precision for given currency code, or
None for unhandled currency codes, e.g. ``func('USD')`` could
return ``Decimal('0.01')``.
"""
assert callable(precision_provider)
global _precision_provider
_precision_provider = precision_provider
_precision_provider = babel_precision_provider.get_precision
def _get_precision(currency, digits):
if digits is None:
return _precision_provider(currency) or DEFAULT_PRECISION
precision = _digits_to_precision.get(digits)
if precision is None:
precision = decimal.Decimal("0.1") ** digits
_digits_to_precision[digits] = precision
return precision
_digits_to_precision = {2: decimal.Decimal("0.01")}