/beancount/core/convert_test.py

https://github.com/beancount/beancount · Python · 321 lines · 199 code · 62 blank · 60 comment · 3 complexity · 640a7086ffa42cf3cee0ac1db4140e63 MD5 · raw file

  1. """Unit tests for conversion functions.
  2. """
  3. __copyright__ = "Copyright (C) 2014-2017 Martin Blais"
  4. __license__ = "GNU GPLv2"
  5. import datetime
  6. import unittest
  7. from beancount.core.number import MISSING
  8. from beancount.core.number import D
  9. from beancount.core.amount import A
  10. from beancount.core.data import create_simple_posting as P
  11. from beancount.core.data import create_simple_posting_with_cost as PCost
  12. from beancount.core.position import Cost
  13. from beancount.core.position import Position
  14. from beancount.core import convert
  15. from beancount.core import inventory
  16. from beancount.core import prices
  17. from beancount.core import data
  18. from beancount import loader
  19. def build_price_map_util(date_currency_price_tuples):
  20. """Build a partial price-map just for testing.
  21. Args:
  22. date_currency_price_tuples: A list of (datetime.date, currency-string,
  23. price-Amount) tuples to fill in the database with.
  24. Returns:
  25. A price_map, as per build_price_map().
  26. """
  27. return prices.build_price_map(
  28. [data.Price(None, date, currency, price)
  29. for date, currency, price in date_currency_price_tuples])
  30. class TestPositionConversions(unittest.TestCase):
  31. """Test conversions to units, cost, weight and market-value for Position objects."""
  32. def _pos(self, units, cost=None, price=None):
  33. # Note: 'price' is only used in Posting class which derives from this test.
  34. self.assertFalse(price)
  35. return Position(units, cost)
  36. #
  37. # Units
  38. #
  39. def test_units(self):
  40. units = A("100 HOOL")
  41. self.assertEqual(units, convert.get_units(
  42. self._pos(units,
  43. Cost(D("514.00"), "USD", None, None))))
  44. self.assertEqual(units, convert.get_units(
  45. self._pos(units, None)))
  46. #
  47. # Cost
  48. #
  49. def test_cost__empty(self):
  50. self.assertEqual(A("100 HOOL"), convert.get_cost(
  51. self._pos(A("100 HOOL"), None)))
  52. def test_cost__not_empty(self):
  53. self.assertEqual(A("51400.00 USD"), convert.get_cost(
  54. self._pos(A("100 HOOL"),
  55. Cost(D("514.00"), "USD", None, None))))
  56. def test_cost__missing(self):
  57. self.assertEqual(A("100 HOOL"), convert.get_cost(
  58. self._pos(A("100 HOOL"),
  59. Cost(MISSING, "USD", None, None))))
  60. #
  61. # Weight
  62. #
  63. def test_weight__no_cost(self):
  64. self.assertEqual(A("100 HOOL"), convert.get_weight(
  65. self._pos(A("100 HOOL"), None)))
  66. def test_weight__with_cost(self):
  67. self.assertEqual(A("51400.00 USD"), convert.get_weight(
  68. self._pos(A("100 HOOL"),
  69. Cost(D("514.00"), "USD", None, None))))
  70. def test_weight__with_cost_missing(self):
  71. self.assertEqual(A("100 HOOL"), convert.get_weight(
  72. self._pos(A("100 HOOL"),
  73. Cost(MISSING, "USD", None, None))))
  74. def test_old_test(self):
  75. # Entry without cost, without price.
  76. posting = P(None, "Assets:Bank:Checking", "105.50", "USD")
  77. self.assertEqual(A("105.50 USD"),
  78. convert.get_weight(posting))
  79. # Entry without cost, with price.
  80. posting = posting._replace(price=A("0.90 CAD"))
  81. self.assertEqual(A("94.95 CAD"),
  82. convert.get_weight(posting))
  83. # Entry with cost, without price.
  84. posting = PCost(None, "Assets:Bank:Checking", "105.50", "USD", "0.80", "EUR")
  85. self.assertEqual(A("84.40 EUR"),
  86. convert.get_weight(posting))
  87. # Entry with cost, and with price (the price should be ignored).
  88. posting = posting._replace(price=A("2.00 CAD"))
  89. self.assertEqual(A("84.40 EUR"),
  90. convert.get_weight(posting))
  91. #
  92. # Value
  93. #
  94. PRICE_MAP_EMPTY = build_price_map_util([])
  95. PRICE_MAP_HIT = build_price_map_util([
  96. (datetime.date(2016, 2, 1), "HOOL", A("530.00 USD")),
  97. (datetime.date(2016, 2, 1), "USD", A("1.2 CAD")),
  98. ])
  99. def test_value__no_currency(self):
  100. pos = self._pos(A("100 HOOL"), None)
  101. self.assertEqual(A("100 HOOL"),
  102. convert.get_value(pos, self.PRICE_MAP_EMPTY))
  103. self.assertEqual(A("100 HOOL"),
  104. convert.get_value(pos, self.PRICE_MAP_HIT))
  105. def test_value__currency_from_cost(self):
  106. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  107. self.assertEqual(A("53000.00 USD"),
  108. convert.get_value(pos, self.PRICE_MAP_HIT))
  109. self.assertEqual(A("100 HOOL"),
  110. convert.get_value(pos, self.PRICE_MAP_EMPTY))
  111. #
  112. # Conversion to another currency.
  113. #
  114. def test_convert_position__success(self):
  115. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  116. self.assertEqual(A("53000.00 USD"),
  117. convert.convert_position(pos, "USD", self.PRICE_MAP_HIT))
  118. def test_convert_position__miss_but_same_currency(self):
  119. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  120. self.assertEqual(A("100 HOOL"),
  121. convert.convert_position(pos, "USD", self.PRICE_MAP_EMPTY))
  122. def test_convert_position__miss_and_miss_rate_to_rate(self):
  123. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  124. self.assertEqual(A("100 HOOL"),
  125. convert.convert_position(pos, "JPY", self.PRICE_MAP_HIT))
  126. PRICE_MAP_RATEONLY = build_price_map_util([
  127. (datetime.date(2016, 2, 1), "USD", A("1.2 CAD")),
  128. ])
  129. def test_convert_position__miss_and_miss_value_rate(self):
  130. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  131. self.assertEqual(A("100 HOOL"),
  132. convert.convert_position(pos, "CAD", self.PRICE_MAP_RATEONLY))
  133. def test_convert_position__miss_and_miss_both(self):
  134. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  135. self.assertEqual(A("100 HOOL"),
  136. convert.convert_position(pos, "CAD", self.PRICE_MAP_EMPTY))
  137. def test_convert_position__miss_and_success_on_implieds(self):
  138. pos = self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None, None))
  139. self.assertEqual(A("63600.00 CAD"),
  140. convert.convert_position(pos, "CAD", self.PRICE_MAP_HIT))
  141. #
  142. # Conversion of amounts to another currency.
  143. #
  144. def test_convert_amount__fail(self):
  145. amt = A("127.00 USD")
  146. self.assertEqual(amt,
  147. convert.convert_amount(amt, "CAD", self.PRICE_MAP_EMPTY))
  148. def test_convert_amount__success(self):
  149. amt = A("127.00 USD")
  150. self.assertEqual(A("152.40 CAD"),
  151. convert.convert_amount(amt, "CAD", self.PRICE_MAP_HIT))
  152. def test_convert_amount__noop(self):
  153. amt = A("127.00 USD")
  154. self.assertEqual(amt,
  155. convert.convert_amount(amt, "USD", self.PRICE_MAP_EMPTY))
  156. self.assertEqual(amt,
  157. convert.convert_amount(amt, "USD", self.PRICE_MAP_HIT))
  158. @loader.load_doc()
  159. def test_convert_amount_with_date(self, entries, _, __):
  160. """
  161. 2013-01-01 price USD 1.20 CAD
  162. 2014-01-01 price USD 1.25 CAD
  163. 2015-01-01 price USD 1.30 CAD
  164. """
  165. price_map = prices.build_price_map(entries)
  166. for date, exp_amount in [
  167. (None, A('130 CAD')),
  168. (datetime.date(2015, 1, 1), A('130 CAD')),
  169. (datetime.date(2014, 12, 31), A('125 CAD')),
  170. (datetime.date(2014, 1, 1), A('125 CAD')),
  171. (datetime.date(2013, 12, 31), A('120 CAD')),
  172. (datetime.date(2013, 1, 1), A('120 CAD')),
  173. (datetime.date(2012, 12, 31), A('100 USD')),
  174. ]:
  175. self.assertEqual(exp_amount,
  176. convert.convert_amount(A('100 USD'), 'CAD', price_map, date))
  177. class TestPostingConversions(TestPositionConversions):
  178. """Test conversions to units, cost, weight and market-value for Posting objects."""
  179. def _pos(self, units, cost=None, price=None):
  180. # Create a Posting instance instead of a Position.
  181. return data.Posting("Assets:AccountA", units, cost, price, None, None)
  182. def test_weight_with_cost_and_price(self):
  183. self.assertEqual(A("51400.00 USD"), convert.get_weight(
  184. self._pos(A("100 HOOL"),
  185. Cost(D("514.00"), "USD", A("530.00 USD"), None))))
  186. def test_weight_with_only_price(self):
  187. self.assertEqual(A("53000.00 USD"), convert.get_weight(
  188. self._pos(A("100 HOOL"), None, A("530.00 USD"))))
  189. def test_value__currency_from_price(self):
  190. pos = self._pos(A("100 HOOL"), None, A("520.00 USD"))
  191. self.assertEqual(A("53000.00 USD"),
  192. convert.get_value(pos, self.PRICE_MAP_HIT))
  193. self.assertEqual(A("100 HOOL"),
  194. convert.get_value(pos, self.PRICE_MAP_EMPTY))
  195. def test_convert_position__currency_from_price(self):
  196. pos = self._pos(A("100 HOOL"), None, A("99999 USD"))
  197. self.assertEqual(A("63600.00 CAD"),
  198. convert.convert_position(pos, "CAD", self.PRICE_MAP_HIT))
  199. class TestMarketValue(unittest.TestCase):
  200. @loader.load_doc()
  201. def setUp(self, entries, _, __):
  202. """
  203. 2013-06-01 price USD 1.01 CAD
  204. 2013-06-05 price USD 1.05 CAD
  205. 2013-06-06 price USD 1.06 CAD
  206. 2013-06-07 price USD 1.07 CAD
  207. 2013-06-10 price USD 1.10 CAD
  208. 2013-06-01 price HOOL 101.00 USD
  209. 2013-06-05 price HOOL 105.00 USD
  210. 2013-06-06 price HOOL 106.00 USD
  211. 2013-06-07 price HOOL 107.00 USD
  212. 2013-06-10 price HOOL 110.00 USD
  213. 2013-06-01 price AAPL 91.00 USD
  214. 2013-06-05 price AAPL 95.00 USD
  215. 2013-06-06 price AAPL 96.00 USD
  216. 2013-06-07 price AAPL 97.00 USD
  217. 2013-06-10 price AAPL 90.00 USD
  218. """
  219. self.price_map = prices.build_price_map(entries)
  220. def test_no_change(self):
  221. balances = inventory.from_string('100 USD')
  222. market_value = balances.reduce(convert.get_value, self.price_map,
  223. datetime.date(2013, 6, 6))
  224. self.assertEqual(inventory.from_string('100 USD'), market_value)
  225. def test_other_currency(self):
  226. balances = inventory.from_string('100 CAD')
  227. market_value = balances.reduce(convert.get_value, self.price_map,
  228. datetime.date(2013, 6, 6))
  229. self.assertEqual(inventory.from_string('100 CAD'), market_value)
  230. def test_mixed_currencies(self):
  231. balances = inventory.from_string('100 USD, 90 CAD')
  232. market_value = balances.reduce(convert.get_value, self.price_map,
  233. datetime.date(2013, 6, 6))
  234. self.assertEqual(inventory.from_string('100 USD, 90 CAD'), market_value)
  235. def test_stock_single(self):
  236. balances = inventory.from_string('5 HOOL {0.01 USD}')
  237. market_value = balances.reduce(convert.get_value, self.price_map,
  238. datetime.date(2013, 6, 6))
  239. self.assertEqual(inventory.from_string('530 USD'), market_value)
  240. def test_stock_many_lots(self):
  241. balances = inventory.from_string('2 HOOL {0.01 USD}, 3 HOOL {0.02 USD}')
  242. market_value = balances.reduce(convert.get_value, self.price_map,
  243. datetime.date(2013, 6, 6))
  244. self.assertEqual(inventory.from_string('530 USD'), market_value)
  245. def test_stock_different_ones(self):
  246. balances = inventory.from_string('2 HOOL {0.01 USD}, 2 AAPL {0.02 USD}')
  247. market_value = balances.reduce(convert.get_value, self.price_map,
  248. datetime.date(2013, 6, 6))
  249. self.assertEqual(inventory.from_string('404 USD'), market_value)
  250. def test_stock_not_found(self):
  251. balances = inventory.from_string('2 MSFT {0.01 USD}')
  252. market_value = balances.reduce(convert.get_value, self.price_map,
  253. datetime.date(2013, 6, 6))
  254. self.assertEqual(inventory.from_string('2 MSFT'), market_value)
  255. if __name__ == '__main__':
  256. unittest.main()