Thermometer: show current monthly income in entire euros
[blender-dev-fund.git] / looper / tests / test_money.py
1 from unittest import TestCase
2
3 import django.test
4
5 from looper.money import *
6
7 EUR: typing.Callable[[int], Money] = functools.partial(Money, 'EUR')
8 USD: typing.Callable[[int], Money] = functools.partial(Money, 'USD')
9
10
11 class BasicOperationsTest(TestCase):
12     def test_properties(self) -> None:
13         self.assertEqual('EUR', EUR(414).currency)
14         self.assertEqual(414, EUR(414).cents)
15
16     def test_bad_currency(self) -> None:
17         # Unfortunately, to support the MoneyField on new objects, we
18         # have to support currency-less Money instances.
19         self.assertEqual(55, Money('', 55).cents)
20
21         with self.assertRaises(TypeError):
22             Money(44, 55)  # type: ignore
23         with self.assertRaises(TypeError):
24             Money(None, 55)  # type: ignore
25
26     def test_bad_cents(self) -> None:
27         with self.assertRaises(TypeError):
28             Money('EUR', 44.3)  # type: ignore
29         with self.assertRaises(TypeError):
30             Money('EUR', 44j)  # type: ignore
31
32     def test_str(self) -> None:
33         self.assertEqual('EUR\u00A05.00', str(EUR(500)))
34         self.assertEqual('USD\u00A00.01', str(Money('USD', 1)))
35
36     def test_repr(self) -> None:
37         self.assertEqual("Money(currency='EUR', cents=12344)", repr(EUR(12344)))
38
39     def test_arithmetic_happy(self) -> None:
40         self.assertEqual(EUR(-4315), +EUR(-4315))
41         self.assertEqual(EUR(-4315), -EUR(4315))
42         self.assertEqual(EUR(4315), EUR(315) + EUR(4000))
43         self.assertEqual(EUR(-3685), EUR(315) - EUR(4000))
44         self.assertEqual(EUR(15), 3 * EUR(5))
45         self.assertEqual(EUR(15), EUR(5) * 3)
46
47     def test_ordering(self) -> None:
48         self.assertLess(EUR(15), EUR(16))
49         self.assertLessEqual(EUR(15), EUR(16))
50         self.assertLessEqual(EUR(15), EUR(15))
51         self.assertGreater(EUR(16), EUR(15))
52         self.assertGreaterEqual(EUR(16), EUR(15))
53         self.assertGreaterEqual(EUR(15), EUR(15))
54         self.assertGreaterEqual(EUR(15), -EUR(100))
55
56         with self.assertRaises(TypeError):
57             EUR(15) < 30
58
59     def test_arithmetic_unsupported(self):
60         m = EUR(56123)
61         with self.assertRaises(TypeError):
62             m + 3
63         with self.assertRaises(TypeError):
64             3 + m
65         with self.assertRaises(TypeError):
66             m - 3
67         with self.assertRaises(TypeError):
68             3 - m
69         with self.assertRaises(TypeError):
70             m * m
71         with self.assertRaises(TypeError):
72             3.0 * m
73         with self.assertRaises(TypeError):
74             m * 3.0
75
76     def test_immutability(self) -> None:
77         m1 = EUR(4123)
78         m2 = m1
79         m1 += EUR(1)
80         self.assertEqual(EUR(4124), m1)
81         self.assertEqual(EUR(4123), m2)
82
83     def test_true_division(self) -> None:
84         self.assertEqual([EUR(4), EUR(4), EUR(3), EUR(3)], EUR(14) / 4)
85
86         with self.assertRaises(TypeError):
87             EUR(14) / 4.0  # type: ignore
88         with self.assertRaises(ValueError):
89             EUR(14) / -4
90         with self.assertRaises(ZeroDivisionError):
91             EUR(14) / 0
92
93     def test_floor_division(self) -> None:
94         self.assertEqual(EUR(3), EUR(14) // 4)
95
96         self.assertEqual(EUR(3), EUR(14) // 4.0)
97         self.assertEqual(EUR(3), EUR(14) // 4.6)
98         self.assertEqual(EUR(2), EUR(14) // 4.7)
99
100         with self.assertRaises(TypeError):
101             EUR(14) // 4.0j  # type: ignore
102         with self.assertRaises(TypeError):
103             EUR(14) // EUR(4)  # type: ignore
104         with self.assertRaises(ValueError):
105             EUR(14) // -4
106         with self.assertRaises(ZeroDivisionError):
107             EUR(14) // 0
108
109     def test_ratio(self):
110         self.assertEqual(0.0, EUR(0) / EUR(4))
111         self.assertEqual(1.0, EUR(4) / EUR(4))
112         self.assertEqual(-2.0, EUR(4) / EUR(-2))
113         self.assertAlmostEqual(1.15, USD(17332) / USD(15072), places=3)
114
115         with self.assertRaises(CurrencyMismatch):
116             EUR(14) / USD(14)
117
118     def test_equality(self) -> None:
119         self.assertEqual(hash(('EUR', 513)), hash(EUR(513)))
120         m1 = EUR(551)
121         m2 = EUR(551)
122         self.assertIsNot(m1, m2)
123         self.assertEqual(m1, m2)
124         self.assertLess(m1, EUR(1000))
125         self.assertNotEqual(m1, 'EUR\u00A0551')
126
127     def test_bool(self) -> None:
128         self.assertTrue(EUR(1))
129         self.assertTrue(EUR(-1))
130         self.assertFalse(EUR(0))
131
132     def test_currency_mismatch(self) -> None:
133         with self.assertRaises(CurrencyMismatch):
134             EUR(14) + USD(44)
135         with self.assertRaises(CurrencyMismatch):
136             EUR(14) - USD(44)
137
138         with self.assertRaises(CurrencyMismatch):
139             EUR(14) < USD(14)
140
141     def test_as_decimal_string(self) -> None:
142         m1 = EUR(1000)
143         self.assertEqual(m1.decimals_string, '10.00')
144         m2 = EUR(9999)
145         self.assertEqual(m2.decimals_string, '99.99')
146         m2 = EUR(1010)
147         self.assertEqual(m2.decimals_string, '10.10')
148         m2 = EUR(1009)
149         self.assertEqual(m2.decimals_string, '10.09')
150         m2 = EUR(1033)
151         self.assertEqual(m2.decimals_string, '10.33')
152
153     def test_with_currency_symbol(self) -> None:
154         self.assertEqual('€\u00A010.00', EUR(1000).with_currency_symbol())
155         self.assertEqual('$\u00A010.00', USD(1000).with_currency_symbol())
156         self.assertEqual('¥\u00A010.00', Money('JPY', 1000).with_currency_symbol())
157
158     def test_with_currency_symbol_nonocents(self) -> None:
159         self.assertEqual('€\u00A010', EUR(1000).with_currency_symbol_nonocents())
160         self.assertEqual('$\u00A010.50', USD(1050).with_currency_symbol_nonocents())
161         self.assertEqual('¥\u00A00.99', Money('JPY', 99).with_currency_symbol_nonocents())
162
163     def test_with_currency_symbol_rounded(self) -> None:
164         self.assertEqual('€\u00A010', EUR(1000).with_currency_symbol_rounded())
165         self.assertEqual('$\u00A010', USD(1050).with_currency_symbol_rounded())
166         self.assertEqual('$\u00A011', USD(1051).with_currency_symbol_rounded())
167         self.assertEqual('$\u00A011', USD(1149).with_currency_symbol_rounded())
168         self.assertEqual('$\u00A012', USD(1150).with_currency_symbol_rounded())
169         self.assertEqual('¥\u00A01', Money('JPY', 99).with_currency_symbol_rounded())
170
171
172 class SumTest(TestCase):
173     def test_empty(self) -> None:
174         self.assertEqual({}, sum_per_currency([]))
175
176     def test_one_currency_one_amount(self) -> None:
177         self.assertEqual({'HRK': Money('HRK', 47)}, sum_per_currency([Money('HRK', 47)]))
178
179     def test_one_currency_multiple_amounts(self) -> None:
180         self.assertEqual({'HRK': Money('HRK', 374)},
181                          sum_per_currency([Money('HRK', 47), Money('HRK', 327)]))
182
183     def test_multiple_currencies(self) -> None:
184         self.assertEqual(
185             {'HRK': Money('HRK', 47 + 327),
186              'SGD': Money('SGD', 413 + 385)},
187             sum_per_currency([
188                 Money('HRK', 47),
189                 Money('HRK', 327),
190                 Money('SGD', 413),
191                 Money('SGD', 385),
192             ])
193         )
194
195         self.assertEqual(
196             {'HRK': Money('HRK', 47 + 327),
197              'SGD': Money('SGD', 413 + 385),
198              'USD': Money('USD', 161)},
199             sum_per_currency([
200                 Money('HRK', 327),
201                 Money('SGD', 413),
202                 Money('HRK', 47),
203                 Money('USD', 161),
204                 Money('SGD', 385),
205             ])
206         )
207
208
209 @django.test.override_settings(
210     LOOPER_CONVERTION_RATES_FROM_EURO={
211         'EUR': 1.0,
212         'USD': 1.15,  # US$ 1.15 = € 1.00
213     }
214 )
215 class SumToEuroTest(django.test.TestCase):
216     def test_empty(self) -> None:
217         self.assertEqual(Money('EUR', 0), sum_to_euros([]))
218
219     def test_single_euro(self) -> None:
220         self.assertEqual(Money('EUR', 215), sum_to_euros([Money('EUR', 215)]))
221
222     def test_multiple_euro(self) -> None:
223         self.assertEqual(Money('EUR', 315), sum_to_euros([Money('EUR', 215), Money('EUR', 100)]))
224
225     def test_multiple_currencies(self) -> None:
226         self.assertEqual(Money('EUR', 287), sum_to_euros([Money('EUR', 200), Money('USD', 100)]))
227
228
229 @django.test.override_settings(
230     LOOPER_CONVERTION_RATES_FROM_EURO={
231         'EUR': 1.0,
232         'USD': 1.15,  # US$ 1.15 = € 1.00
233         'SGD': 1.59,  # SG$ 1.59 = € 1.00
234     }
235 )
236 class ConvertCurrencyTest(django.test.TestCase):
237     def test_identical(self) -> None:
238         amt = Money('EUR', 47)
239         self.assertIs(amt, convert_currency(amt, to_currency='EUR'))
240
241     def test_usd_to_eur(self) -> None:
242         self.assertEqual(Money('EUR', 435), convert_currency(Money('USD', 500), to_currency='EUR'))
243
244     def test_eur_to_usd(self) -> None:
245         self.assertEqual(Money('USD', 500), convert_currency(Money('EUR', 435), to_currency='USD'))
246
247     def test_sgd_to_usd(self) -> None:
248         self.assertEqual(Money('USD', 34230),
249                          convert_currency(Money('SGD', 47327), to_currency='USD'))
250
251
252 class PickleMoneyTest(TestCase):
253     def test_pickle(self) -> None:
254         import pickle
255
256         amt = Money('HRK', 716)
257         pickled = pickle.dumps(amt)
258         unpickled = pickle.loads(pickled)
259
260         self.assertEqual(amt, unpickled)