Changeset View
Changeset View
Standalone View
Standalone View
looper/money.py
| Show All 38 Lines | class Money: | ||||
| @property | @property | ||||
| def just_whole(self) -> int: | def just_whole(self) -> int: | ||||
| """Return the whole currency, so € 1.23 → 1""" | """Return the whole currency, so € 1.23 → 1""" | ||||
| return self._cents // 100 | return self._cents // 100 | ||||
| @property | @property | ||||
| def currency_symbol(self) -> str: | def currency_symbol(self) -> str: | ||||
| return babel.numbers.get_currency_symbol( | symbol = babel.numbers.get_currency_symbol( | ||||
| self._currency, locale=settings.LOOPER_MONEY_LOCALE | self._currency, locale=settings.LOOPER_MONEY_LOCALE | ||||
| ) | ) | ||||
| assert isinstance(symbol, str) | |||||
| return symbol | |||||
| @property | @property | ||||
| def decimals_string(self) -> str: | def decimals_string(self) -> str: | ||||
| """Return the amount as a string, without currency, and with a decimal point. | """Return the amount as a string, without currency, and with a decimal point. | ||||
| >>> Money('EUR', 1033).decimals_string | >>> Money('EUR', 1033).decimals_string | ||||
| '10.33' | '10.33' | ||||
| """ | """ | ||||
| Show All 39 Lines | def with_currency_symbol_nonocents(self) -> str: | ||||
| return f'{self.currency_symbol}\u00A0{whole}' | return f'{self.currency_symbol}\u00A0{whole}' | ||||
| def __pos__(self) -> 'Money': | def __pos__(self) -> 'Money': | ||||
| return Money(self._currency, self._cents) | return Money(self._currency, self._cents) | ||||
| def __neg__(self) -> 'Money': | def __neg__(self) -> 'Money': | ||||
| return Money(self._currency, -self._cents) | return Money(self._currency, -self._cents) | ||||
| def _assert_same_currency(self, other) -> None: | def _assert_same_currency(self, other: 'Money') -> None: | ||||
| if not isinstance(other, Money): | if not isinstance(other, Money): | ||||
| raise TypeError(f'{other!r} is not {Money!r}') | raise TypeError(f'{other!r} is not {Money!r}') | ||||
| if self._currency == other._currency: | if self._currency == other._currency: | ||||
| return | return | ||||
| raise CurrencyMismatch(f'Currency mismatch between {self} and {other}') | raise CurrencyMismatch(f'Currency mismatch between {self} and {other}') | ||||
| def __add__(self, other: 'Money') -> 'Money': | def __add__(self, other: 'Money') -> 'Money': | ||||
| self._assert_same_currency(other) | self._assert_same_currency(other) | ||||
| return Money(self._currency, self._cents + other._cents) | return Money(self._currency, self._cents + other._cents) | ||||
| def __sub__(self, other: 'Money') -> 'Money': | def __sub__(self, other: 'Money') -> 'Money': | ||||
| self._assert_same_currency(other) | self._assert_same_currency(other) | ||||
| return Money(self._currency, self._cents - other._cents) | return Money(self._currency, self._cents - other._cents) | ||||
| def __mul__(self, other) -> 'Money': | def __mul__(self, other: int) -> 'Money': | ||||
| if isinstance(other, Money): | if isinstance(other, Money): | ||||
| raise TypeError('cannot multiply monetary quantities') | raise TypeError('cannot multiply monetary quantities') | ||||
| if not isinstance(other, int): | if not isinstance(other, int): | ||||
| raise TypeError(f'unsupported type {type(other)}') | raise TypeError(f'unsupported type {type(other)}') | ||||
| return Money(self._currency, self._cents * other) | return Money(self._currency, self._cents * other) | ||||
| __radd__ = __add__ | __radd__ = __add__ | ||||
| __rmul__ = __mul__ | __rmul__ = __mul__ | ||||
| def __truediv__(self, divisor: Union[int, 'Money']) -> Union[list, float]: | @overload | ||||
| def __truediv__(self, divisor: int) -> List['Money']: | |||||
| # TODO(anna): Remove this overload. Instead just create a function | |||||
| # `segment(x: int) -> List[Money]` or something. | |||||
| ... | |||||
| @overload | |||||
| def __truediv__(self, divisor: 'Money') -> float: | |||||
| ... | |||||
| def __truediv__(self, divisor: Union[int, 'Money']) -> Union[List['Money'], float]: | |||||
| """Split up the amount in (almost-)equal parts or compute ratio. | """Split up the amount in (almost-)equal parts or compute ratio. | ||||
| When the divisor is an integer, return a list of `divisor` Money | When the divisor is an integer, return a list of `divisor` Money | ||||
| objects that sum to `self`. | objects that sum to `self`. | ||||
| When the divisor is a Money object, return the ratio between 'self' | When the divisor is a Money object, return the ratio between 'self' | ||||
| and 'divisor'. This requires both Money objects to have the same | and 'divisor'. This requires both Money objects to have the same | ||||
| currency. | currency. | ||||
| ▲ Show 20 Lines • Show All 44 Lines • ▼ Show 20 Lines | def __floordiv__(self, divisor: Union[int, float]) -> 'Money': | ||||
| divided_cents = int(self._cents // divisor) | divided_cents = int(self._cents // divisor) | ||||
| return self.__class__(self._currency, divided_cents) | return self.__class__(self._currency, divided_cents) | ||||
| def _ratio(self, other: 'Money') -> float: | def _ratio(self, other: 'Money') -> float: | ||||
| self._assert_same_currency(other) | self._assert_same_currency(other) | ||||
| return self.cents / other.cents | return self.cents / other.cents | ||||
| def __eq__(self, other) -> bool: | def __eq__(self, other: object) -> bool: | ||||
| if not isinstance(other, Money): | if not isinstance(other, Money): | ||||
| return False | return False | ||||
| return self._cents == other._cents and self._currency == other._currency | return self._cents == other._cents and self._currency == other._currency | ||||
| def __ne__(self, other) -> bool: | def __ne__(self, other: object) -> bool: | ||||
| return not self.__eq__(other) | return not self.__eq__(other) | ||||
| def __hash__(self) -> int: | def __hash__(self) -> int: | ||||
| return hash((self._currency, self._cents)) | return hash((self._currency, self._cents)) | ||||
| def __lt__(self, other): | def __lt__(self, other: object) -> bool: | ||||
| if not isinstance(other, Money): | if not isinstance(other, Money): | ||||
| return NotImplemented | return NotImplemented | ||||
| self._assert_same_currency(other) | self._assert_same_currency(other) | ||||
| return self._cents < other._cents | return self._cents < other._cents | ||||
| def __bool__(self) -> bool: | def __bool__(self) -> bool: | ||||
| return self._cents != 0 | return self._cents != 0 | ||||
| Show All 19 Lines | def sum_to_euros(amounts: Iterable[Money]) -> Money: | ||||
| """Sums all amounts while converting to €. | """Sums all amounts while converting to €. | ||||
| Note that all converted amounts are rounded to entire cents, so | Note that all converted amounts are rounded to entire cents, so | ||||
| this is a lossy operation. | this is a lossy operation. | ||||
| """ | """ | ||||
| from django.conf import settings | from django.conf import settings | ||||
| rates = settings.LOOPER_CONVERTION_RATES_FROM_EURO | rates: Dict[str, float] = settings.LOOPER_CONVERTION_RATES_FROM_EURO | ||||
| sum_cents_eur = 0 | sum_cents_eur = 0 | ||||
| for amount in amounts: | for amount in amounts: | ||||
| conversion_rate = rates[amount.currency] | conversion_rate = rates[amount.currency] | ||||
| converted_cents = int(round(amount.cents / conversion_rate)) | converted_cents = int(round(amount.cents / conversion_rate)) | ||||
| sum_cents_eur += converted_cents | sum_cents_eur += converted_cents | ||||
| return Money('EUR', sum_cents_eur) | return Money('EUR', sum_cents_eur) | ||||
| def convert_currency(amount: Money, *, to_currency: str) -> Money: | def convert_currency(amount: Money, *, to_currency: str) -> Money: | ||||
| """Convert the amount to another currency. | """Convert the amount to another currency. | ||||
| Note that the converted amount is rounded to entire cents, so | Note that the converted amount is rounded to entire cents, so | ||||
| this is a lossy operation. | this is a lossy operation. | ||||
| """ | """ | ||||
| from django.conf import settings | from django.conf import settings | ||||
| if amount.currency == to_currency: | if amount.currency == to_currency: | ||||
| return amount | return amount | ||||
| rates = settings.LOOPER_CONVERTION_RATES_FROM_EURO | rates: Dict[str, float] = settings.LOOPER_CONVERTION_RATES_FROM_EURO | ||||
| current_to_euro = 1 / rates[amount.currency] | current_to_euro = 1 / rates[amount.currency] | ||||
| euro_to_target = rates[to_currency] | euro_to_target = rates[to_currency] | ||||
| cents_in_eur = amount.cents * current_to_euro | cents_in_eur = amount.cents * current_to_euro | ||||
| cents_in_target = cents_in_eur * euro_to_target | cents_in_target = cents_in_eur * euro_to_target | ||||
| converted_cents = int(round(cents_in_target)) | converted_cents = int(round(cents_in_target)) | ||||
| return Money(to_currency, converted_cents) | return Money(to_currency, converted_cents) | ||||