Exemple #1
0
    def test_add_amount__allow_negative(self):
        inv = Inventory()

        # Test adding positions of different types.
        position_, _ = inv.add_amount(A('-11 USD'))
        self.assertIsNone(position_)
        position_, _ = inv.add_amount(A('-11 USD'),
                                      Cost(D('1.10'), 'CAD', None, None))
        self.assertIsNone(position_)
        position_, _ = inv.add_amount(A('-11 USD'),
                                      Cost(D('1.10'), 'CAD', date(2012, 1, 1), None))
        self.assertIsNone(position_)

        # Check for reductions.
        invlist = list(inv)
        self.assertTrue(invlist[1].is_negative_at_cost())
        self.assertTrue(invlist[2].is_negative_at_cost())
        inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', None, None))
        inv.add_amount(A('-11 USD'), Cost(D('1.10'), 'CAD', date(2012, 1, 1), None))
        self.assertEqual(3, len(inv))

        # Test adding to a position that does exist.
        inv = I('10 USD, 10 USD {1.10 CAD}, 10 USD {1.10 CAD, 2012-01-01}')
        position_, _ = inv.add_amount(A('-11 USD'))
        self.assertEqual(position_, position.from_string('10 USD'))
        position_, _ = inv.add_amount(A('-11 USD'),
                                      Cost(D('1.10'), 'CAD', None, None))
        self.assertEqual(position_, position.from_string('10 USD {1.10 CAD}'))
        position_, _ = inv.add_amount(A('-11 USD'),
                                      Cost(D('1.10'), 'CAD', date(2012, 1, 1), None))
        self.assertEqual(position_, position.from_string('10 USD {1.10 CAD, 2012-01-01}'))
 def test_reducing__sans_date__equal_amount(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('10 HOOL {500 USD}'))
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('-10 HOOL {500 USD}'))
     self.assertTrue(inv.is_empty())
    def test_get_pholding_market_value(self):
        posting = data.Posting('Account',
                               position.from_string('100 MSFT {54.34 USD}'),
                               A('60.00 USD'),
                               None, None)
        self.assertEqual(A('6000.00 USD'),
                         holdings.get_pholding_market_value(posting))

        posting = data.Posting('Account',
                               position.from_string('100 MSFT {54.34 USD}'),
                               None,
                               None, None)
        self.assertEqual(A('5434.00 USD'),
                         holdings.get_pholding_market_value(posting))

        posting = data.Posting('Account',
                               position.from_string('1000.00 USD'),
                               None,
                               None, None)
        self.assertEqual(A('1000.00 USD'),
                         holdings.get_pholding_market_value(posting))

        with self.assertRaises(AssertionError):
            posting = data.Posting('Account',
                                   position.from_string('1000.00 USD'),
                                   A('60.00 USD'),
                                   None, None)
            holdings.get_pholding_market_value(posting)

        with self.assertRaises(AssertionError):
            posting = data.Posting('Account',
                                   position.from_string('1000.00 USD {1.25 CAD}'),
                                   A('60.00 USD'),
                                   None, None)
            holdings.get_pholding_market_value(posting)
 def test_reducing__sans_date__over_amount(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('10 HOOL {500 USD}'))
     with self.assertRaises(ValueError):
         wash_calculator.book_position(
             inv, DD('2014-02-10'),
             position.from_string('-13 HOOL {500 USD}'))
 def test_reducing__with_diff_date__incomplete_amount(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('10 HOOL {500 USD}'))
     with self.assertRaises(ValueError):
         wash_calculator.book_position(
             inv, DD('2014-02-10'),
             position.from_string('-7 HOOL {500 USD / 2014-02-11}'))
Exemple #6
0
    def test_various(self):
        pos = position.from_string('100.00 USD')
        rdr = self.get(pos)
        self.assertEqual('100.00   USD', rdr.format(pos))

        pos = position.from_string('5 HOOL {500.23 USD}')
        rdr = self.get(pos)
        self.assertEqual('5     HOOL {500.23   USD}', rdr.format(pos))
 def test_reducing__sans_date__incomplete_amount(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('10 HOOL {500 USD}'))
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('-7 HOOL {500 USD}'))
     self.assertEqual(
         inventory.from_string('3 HOOL {500 USD / 2014-02-10}'), inv)
 def test_augmenting__sans_date(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'), position.from_string('10 HOOL {500 USD}'))
     wash_calculator.book_position(inv, DD('2014-02-10'),
                                   position.from_string('7 HOOL {500 USD}'))
     self.assertEqual(
         inventory.from_string('17 HOOL {500 USD / 2014-02-10}'), inv)
 def test_get_position(self):
     inv = Inventory(self.POSITIONS_ALL_KINDS)
     self.assertEqual(
         position.from_string('40.50 USD'),
         inv.get_position(Lot('USD', None, None)))
     self.assertEqual(
         position.from_string('40.50 USD {1.10 CAD}'),
         inv.get_position(Lot('USD', A('1.10 CAD'), None)))
     self.assertEqual(
         position.from_string('40.50 USD {1.10 CAD, 2012-01-01}'),
         inv.get_position(Lot('USD', A('1.10 CAD'), date(2012, 1, 1))))
Exemple #10
0
 def test_balance_incomplete_postings__fill2(self):
     entry, errors = self.get_incomplete_entry("""
       2013-02-23 * "Something"
         Liabilities:CreditCard     -50 USD
         Liabilities:CreditCard     -50 CAD
         Expenses:Restaurant
     """)
     self.assertFalse(errors)
     self.assertEqual(4, len(entry.postings))
     self.assertEqual(entry.postings[2].account, 'Expenses:Restaurant')
     self.assertEqual(entry.postings[3].account, 'Expenses:Restaurant')
     self.assertEqual(entry.postings[2].position, position.from_string('50 USD'))
     self.assertEqual(entry.postings[3].position, position.from_string('50 CAD'))
Exemple #11
0
    def test_holding_to_position(self):
        test_holding = holdings.Holding(
            'Assets:US:Checking', D('100'), 'MSFT', D('54.34'), 'USD',
            D('5434.00'), D('6000.00'), D('60'), datetime.date(2012, 5, 2))
        actual_position = holdings.holding_to_position(test_holding)
        expected_position = position.from_string('100 MSFT {54.34 USD}')
        self.assertEqual(expected_position, actual_position)

        test_holding = holdings.Holding(
            'Assets:US:Checking', D('100'), 'USD', None, None,
            None, None, None, datetime.date(2012, 5, 2))
        actual_position = holdings.holding_to_position(test_holding)
        expected_position = position.from_string('100.00 USD')
        self.assertEqual(expected_position, actual_position)
Exemple #12
0
def deserialise_posting(posting):
    """Parse JSON to a Beancount Posting."""
    amount = posting.get("amount")
    unit_price = None
    total_price = None
    if amount:
        if "@@" in amount:
            amount, raw_total_price = amount.split("@@")
            total_price = A(raw_total_price)
        elif "@" in amount:
            amount, raw_unit_price = amount.split("@")
            unit_price = A(raw_unit_price)
        pos = position.from_string(amount)
        units = pos.units
        if re.search(r"{\s*}", amount):
            cost = data.CostSpec(MISSING, None, MISSING, None, None, False)
        else:
            cost = pos.cost
    else:
        units, cost = None, None
    if total_price is not None:
        try:
            num = total_price.number / units.number.copy_abs()
        except decimal.InvalidOperation:
            # if both units and total price is zero, set it to zero.
            num = ZERO
        unit_price = Amount(num, total_price.currency)
    return data.Posting(posting["account"], units, cost, unit_price, None,
                        None)
Exemple #13
0
 def test_position(self):
     itypes = [('pos', position.Position)]
     irows = [(position.from_string(string), )
              for string in self.input_amounts]
     atypes, arows = numberify.numberify_results(itypes, irows)
     self.assertEqual(self.expected_types, atypes)
     self.assertEqual(self.expected_rows, arows)
Exemple #14
0
 def test_render_posting_no_cost(self):
     pos = position.from_string('100 USD')
     str_posting = journal_text.render_posting(
         data.Posting('Assets:Something', pos.units, pos.cost, None, None,
                      None), self.number_format)
     self.assertEqual('  Assets:Something                 100 USD',
                      str_posting)
 def test_creating__with_date(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(
         inv, DD('2014-02-10'),
         position.from_string('10 HOOL {500 USD / 2014-02-11}'))
     self.assertEqual(
         inventory.from_string('10 HOOL {500 USD / 2014-02-11}'), inv)
 def test_render_posting_price(self):
     str_posting = journal_text.render_posting(
         data.Posting('Assets:Something', position.from_string('10 VHT'),
                      A('45.32 USD'), None, None), self.number_format)
     self.assertEqual(
         '  Assets:Something                 10 VHT @ 45.32 USD',
         str_posting)
Exemple #17
0
 def test_render_posting_cost_price(self):
     pos = position.from_string('10 VHT {45.32 USD}')
     str_posting = journal_text.render_posting(
         data.Posting('Assets:Something', pos.units, pos.cost,
                      A('47.00 USD'), None, None), self.number_format)
     self.assertEqual(
         '  Assets:Something                 10 VHT {45.32 USD} @ 47.00 USD',
         str_posting)
Exemple #18
0
 def test_balance_incomplete_postings__cost(self):
     entry, errors = self.get_incomplete_entry("""
       2013-02-23 * "Something"
         Assets:Invest     10 MSFT {43.23 USD}
         Assets:Cash
     """)
     self.assertFalse(errors)
     self.assertEqual(2, len(entry.postings))
     self.assertEqual(entry.postings[1].account, 'Assets:Cash')
     self.assertEqual(entry.postings[1].position, position.from_string('-432.30 USD'))
Exemple #19
0
 def test_balance_incomplete_postings__fill1(self):
     entry, errors = self.get_incomplete_entry("""
       option "booking_algorithm" "SIMPLE"
       2013-02-23 * "Something"
         Liabilities:CreditCard     -50 USD
         Expenses:Restaurant
     """)
     self.assertFalse(errors)
     self.assertEqual(2, len(entry.postings))
     self.assertEqual(position.get_position(entry.postings[1]),
                      position.from_string('50 USD'))
Exemple #20
0
    def test_precision(self):
        # Some display context.
        dcontext = display_context.DisplayContext()
        dcontext.update(D('111'), 'JPY')
        dcontext.update(D('1.111'), 'RGAGX')
        dcontext.update(D('1.11'), 'USD')
        dformat = dcontext.build()

        # Input data.
        itypes = [('number', Decimal),
                  ('amount', amount.Amount),
                  ('position', position.Position),
                  ('inventory', inventory.Inventory)]
        irows = [[D(amt.split()[0]),
                  A(amt),
                  position.from_string(amt),
                  inventory.from_string(amt)]
                 for amt in ['123.45678909876 JPY',
                             '1.67321232123 RGAGX',
                             '5.67345434543 USD']]

        # First check with no explicit quantization.
        atypes, arows = numberify.numberify_results(itypes, irows)
        erows = [[D('123.45678909876'),
                  None, None, D('123.45678909876'),
                  None, None, D('123.45678909876'),
                  None, None, D('123.45678909876')],
                 [D('1.67321232123'),
                  None, D('1.67321232123'), None,
                  None, D('1.67321232123'), None,
                  None, D('1.67321232123'), None],
                 [D('5.67345434543'),
                  D('5.67345434543'), None, None,
                  D('5.67345434543'), None, None,
                  D('5.67345434543'), None, None]]
        self.assertEqual(erows, arows)

        # Then compare with quantization.
        atypes, arows = numberify.numberify_results(itypes, irows, dformat)

        erows = [[D('123.45678909876'),
                  None, None, D('123'),
                  None, None, D('123'),
                  None, None, D('123')],
                 [D('1.67321232123'),
                  None, D('1.673'), None, None,
                  D('1.673'), None, None,
                  D('1.673'), None],
                 [D('5.67345434543'),
                  D('5.67'), None, None,
                  D('5.67'), None, None,
                  D('5.67'), None, None]]
        self.assertEqual(erows, arows)
Exemple #21
0
    def test_balance_incomplete_postings__insert_rounding(self):
        entry, errors = self.get_incomplete_entry("""
          option "account_rounding" "RoundingError"

          2013-02-23 * "Something"
            Assets:Invest     1.245 RGAGX {43.23 USD}
            Assets:Cash      -53.82 USD
        """)
        self.assertFalse(errors)
        self.assertEqual(3, len(entry.postings))
        self.assertEqual(entry.postings[2].account, 'Equity:RoundingError')
        self.assertEqual(entry.postings[2].position, position.from_string('-0.00135 USD'))
Exemple #22
0
def deserialise_posting(posting):
    """Parse JSON to a Beancount Posting."""
    amount = posting.get('amount')
    price = None
    if amount:
        if '@' in amount:
            amount, raw_price = amount.split('@')
            price = A(raw_price)
        pos = position.from_string(amount)
        units, cost = pos.units, pos.cost
    else:
        units, cost = None, None
    return data.Posting(posting['account'], units, cost, price, None, None)
Exemple #23
0
    def test_holding_to_posting(self):
        test_holding = holdings.Holding(
            'Assets:US:Checking', D('100'), 'MSFT', D('54.34'), 'USD',
            D('5434.00'), D('6000.00'), D('60'), datetime.date(2012, 5, 2))

        posting = holdings.holding_to_posting(test_holding)
        self.assertTrue(isinstance(posting, data.Posting))

        expected_position = position.from_string('100 MSFT {54.34 USD}')
        self.assertEqual(expected_position, position.Position(posting.units, posting.cost))

        expected_price = A('60.00 USD')
        self.assertEqual(expected_price, posting.price)
    def test_ctor_empty_len(self):
        # Test regular constructor.
        inv = Inventory()
        self.assertTrue(inv.is_empty())
        self.assertEqual(0, len(inv))

        inv = Inventory([position.from_string('100.00 USD'),
                         position.from_string('101.00 USD')])
        self.assertFalse(inv.is_empty())
        self.assertEqual(1, len(inv))

        inv = Inventory([position.from_string('100.00 USD'),
                         position.from_string('100.00 CAD')])
        self.assertFalse(inv.is_empty())
        self.assertEqual(2, len(inv))

        inv = Inventory()
        self.assertEqual(0, len(inv))
        inv.add_amount(A('100 USD'))
        self.assertEqual(1, len(inv))
        inv.add_amount(A('100 CAD'))
        self.assertEqual(2, len(inv))
Exemple #25
0
def deserialise_posting(posting):
    """Parse JSON to a Beancount Posting."""
    amount = posting.get("amount")
    price = None
    if amount:
        if "@" in amount:
            amount, raw_price = amount.split("@")
            price = A(raw_price)
        pos = position.from_string(amount)
        units = pos.units
        if re.search(r"{\s*}", amount):
            cost = data.CostSpec(MISSING, None, MISSING, None, None, False)
        else:
            cost = pos.cost
    else:
        units, cost = None, None
    return data.Posting(posting["account"], units, cost, price, None, None)
Exemple #26
0
def deserialise_posting(posting):
    """Parse JSON to a Beancount Posting."""
    amount = posting.get('amount')
    price = None
    if amount:
        if '@' in amount:
            amount, raw_price = amount.split('@')
            price = A(raw_price)
        pos = position.from_string(amount)
        units = pos.units
        if re.search(r'{\s*}', amount):
            cost = data.CostSpec(
                MISSING,
                None,
                MISSING,
                None,
                None,
                False
            )
        else:
            cost = pos.cost
    else:
        units, cost = None, None
    return data.Posting(posting['account'], units, cost, price, None, None)
 def test_units(self):
     inv = Inventory(self.POSITIONS_ALL_KINDS +
                     [position.from_string('50.00 CAD')])
     inv_cost = inv.units()
     self.assertEqual(Inventory.from_string('121.50 USD, 50.00 CAD'), inv_cost)
Exemple #28
0
 def test_to_string_no_detail(self):
     pos = from_string('2.2 HOOL {532.43 USD, 2014-06-15}')
     self.assertEqual(('2.2 HOOL {532.43 USD}'),
                      pos.to_string(detail=False))
 def test_cost(self):
     inv = Inventory(self.POSITIONS_ALL_KINDS +
                     [position.from_string('50.00 CAD')])
     inv_cost = inv.cost()
     self.assertEqual(Inventory.from_string('40.50 USD, 139.10 CAD'), inv_cost)
class TestInventory(unittest.TestCase):

    def checkAmount(self, inventory, number, currency):
        if isinstance(number, str):
            number = D(number)
        amount_ = amount.Amount(number, currency)
        inv_amount = inventory.get_units(amount_.currency)
        self.assertEqual(inv_amount, amount_)

    def test_from_string(self):
        inv = inventory.from_string('')
        self.assertEqual(Inventory(), inv)

        inv = inventory.from_string('10 USD')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('10'))]),
            inv)

        inv = inventory.from_string(' 10.00  USD ')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('10'))]),
            inv)

        inv = inventory.from_string('1 USD, 2 CAD')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('1')),
                       Position(Lot("CAD", None, None), D('2'))]),
            inv)

        inv = inventory.from_string('2.2 HOOL {532.43 USD}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('532.43 USD'), None),
                                D('2.2')),
                       Position(Lot("EUR", None, None), D('3.413'))]),
            inv)

        inv = inventory.from_string(
            '2.2 HOOL {532.43 USD}, 2.3 HOOL {564.00 USD, 2015-07-14}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('532.43 USD'), None),
                                D('2.2')),
                       Position(Lot("HOOL", A('564.00 USD'), datetime.date(2015, 7, 14)),
                                D('2.3')),
                       Position(Lot("EUR", None, None),
                                D('3.413'))]),
            inv)

        inv = inventory.from_string(
            '1.1 HOOL {500.00 # 11.00 USD}, 100 CAD')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('510.00 USD'), None),
                                D('1.1')),
                       Position(Lot("CAD", None, None),
                                D('100'))]),
            inv)

    def test_ctor_empty_len(self):
        # Test regular constructor.
        inv = Inventory()
        self.assertTrue(inv.is_empty())
        self.assertEqual(0, len(inv))

        inv = Inventory([position.from_string('100.00 USD'),
                         position.from_string('101.00 USD')])
        self.assertFalse(inv.is_empty())
        self.assertEqual(1, len(inv))

        inv = Inventory([position.from_string('100.00 USD'),
                         position.from_string('100.00 CAD')])
        self.assertFalse(inv.is_empty())
        self.assertEqual(2, len(inv))

        inv = Inventory()
        self.assertEqual(0, len(inv))
        inv.add_amount(A('100 USD'))
        self.assertEqual(1, len(inv))
        inv.add_amount(A('100 CAD'))
        self.assertEqual(2, len(inv))

    def test_str(self):
        inv = Inventory.from_string('100.00 USD, 101.00 CAD')
        self.assertEqual('(100.00 USD, 101.00 CAD)', str(inv))

    def test_copy(self):
        inv = Inventory()
        inv.add_amount(A('100.00 USD'))
        self.checkAmount(inv, '100', 'USD')

        # Test copying.
        inv2 = copy.copy(inv)
        inv2.add_amount(A('50.00 USD'))
        self.checkAmount(inv2, '150', 'USD')

        # Check that the original object is not modified.
        self.checkAmount(inv, '100', 'USD')

    def test_op_eq(self):
        inv1 = Inventory.from_string('100 USD, 100 CAD')
        inv2 = Inventory.from_string('100 CAD, 100 USD')
        self.assertEqual(inv1, inv2)
        self.assertEqual(inv2, inv1)

        inv3 = Inventory.from_string('200 USD, 100 CAD')
        self.assertNotEqual(inv1, inv3)
        self.assertNotEqual(inv3, inv1)

        inv4 = Inventory.from_string('100 USD, 100 JPY')
        self.assertNotEqual(inv1, inv4)
        self.assertNotEqual(inv4, inv1)

        inv5 = Inventory.from_string('100 JPY, 100 USD')
        self.assertEqual(inv4, inv5)

    def test_is_small__value(self):
        test_inv = Inventory.from_string('1.50 JPY, 1.51 USD, 1.52 CAD')
        for inv in test_inv, -test_inv:
            self.assertFalse(inv.is_small(D('1.49')))
            self.assertFalse(inv.is_small(D('1.50')))
            self.assertTrue(inv.is_small(D('1.53')))
            self.assertTrue(inv.is_small(D('1.52')))

    def test_is_small__dict(self):
        test_inv = Inventory.from_string('0.03 JPY, 0.003 USD')
        for inv in test_inv, -test_inv:
            # Test all four types of inequalities.
            self.assertTrue(inv.is_small({'JPY': D('0.05'), 'USD': D('0.005')}))
            self.assertFalse(inv.is_small({'JPY': D('0.005'), 'USD': D('0.0005')}))
            self.assertTrue(inv.is_small({'JPY': D('0.05'), 'USD': D('0.5')}))
            self.assertFalse(inv.is_small({'JPY': D('0.005'), 'USD': D('0.005')}))

            # Test border case and an epsilon under.
            self.assertTrue(inv.is_small({'JPY': D('0.03'), 'USD': D('0.003')}))
            self.assertFalse(inv.is_small({'JPY': D('0.02999999999999'),
                                           'USD': D('0.003')}))
            self.assertFalse(inv.is_small({'JPY': D('0.03'), 'USD': D('0.00299999')}))

            # Test missing precisions.
            self.assertFalse(inv.is_small({'JPY': D('0.05')}))
            self.assertFalse(inv.is_small({'USD': D('0.005')}))

            # Test extra precisions.
            self.assertTrue(inv.is_small({'JPY': D('0.05'),
                                          'USD': D('0.005'),
                                          'CAD': D('0.0005')}))

            # Test no precisions.
            self.assertFalse(inv.is_small({}))

    def test_is_small__with_default(self):
        inv = Inventory.from_string('0.03 JPY')
        self.assertTrue(inv.is_small({'JPY': D('0.05')}))
        self.assertFalse(inv.is_small({'JPY': D('0.02')}))
        self.assertTrue(inv.is_small({}, {'JPY': D('0.05')}))
        self.assertFalse(inv.is_small({}, {'JPY': D('0.02')}))

    def test_is_mixed(self):
        inv = Inventory.from_string('100 HOOL {250 USD}, 101 HOOL {251 USD}')
        self.assertFalse(inv.is_mixed())

        inv = Inventory.from_string('100 HOOL {250 USD}, -1 HOOL {251 USD}')
        self.assertTrue(inv.is_mixed())

        inv = Inventory.from_string('-2 HOOL {250 USD}, -1 HOOL {251 USD}')
        self.assertFalse(inv.is_mixed())

    def test_op_neg(self):
        inv = Inventory()
        inv.add_amount(A('10 USD'))
        ninv = -inv
        self.checkAmount(ninv, '-10', 'USD')

        pinv = Inventory.from_string('1.50 JPY, 1.51 USD, 1.52 CAD')
        ninv = Inventory.from_string('-1.50 JPY, -1.51 USD, -1.52 CAD')
        self.assertEqual(pinv, -ninv)

    def test_get_units(self):
        inv = Inventory.from_string('40.50 JPY, 40.51 USD {1.01 CAD}, 40.52 CAD')
        self.assertEqual(inv.get_units('JPY'), A('40.50 JPY'))
        self.assertEqual(inv.get_units('USD'), A('40.51 USD'))
        self.assertEqual(inv.get_units('CAD'), A('40.52 CAD'))
        self.assertEqual(inv.get_units('AUD'), A('0 AUD'))
        self.assertEqual(inv.get_units('NZD'), A('0 NZD'))

    def test_units1(self):
        inv = Inventory()
        self.assertEqual(inv.units(), Inventory.from_string(''))

        inv = Inventory.from_string('40.50 JPY, 40.51 USD {1.01 CAD}, 40.52 CAD')
        self.assertEqual(inv.units(),
                         Inventory.from_string('40.50 JPY, 40.51 USD, 40.52 CAD'))

        # Check that the same units coalesce.
        inv = Inventory.from_string('2 HOOL {400 USD}, 3 HOOL {410 USD}')
        self.assertEqual(inv.units(), Inventory.from_string('5 HOOL'))

        inv = Inventory.from_string('2 HOOL {400 USD}, -3 HOOL {410 USD}')
        self.assertEqual(inv.units(), Inventory.from_string('-1 HOOL'))

    POSITIONS_ALL_KINDS = [
        position.from_string('40.50 USD'),
        position.from_string('40.50 USD {1.10 CAD}'),
        position.from_string('40.50 USD {1.10 CAD, 2012-01-01}')]

    def test_units(self):
        inv = Inventory(self.POSITIONS_ALL_KINDS +
                        [position.from_string('50.00 CAD')])
        inv_cost = inv.units()
        self.assertEqual(Inventory.from_string('121.50 USD, 50.00 CAD'), inv_cost)

    def test_cost(self):
        inv = Inventory(self.POSITIONS_ALL_KINDS +
                        [position.from_string('50.00 CAD')])
        inv_cost = inv.cost()
        self.assertEqual(Inventory.from_string('40.50 USD, 139.10 CAD'), inv_cost)

    def test_average(self):
        # Identity, no aggregation.
        inv = Inventory.from_string('40.50 JPY, 40.51 USD {1.01 CAD}, 40.52 CAD')
        self.assertEqual(inv.average(), inv)

        # Identity, no aggregation, with a mix of lots at cost and without cost.
        inv = Inventory.from_string('40 USD {1.01 CAD}, 40 USD')
        self.assertEqual(inv.average(), inv)

        # Aggregation.
        inv = Inventory.from_string('40 USD {1.01 CAD}, 40 USD {1.02 CAD}')
        self.assertEqual(inv.average(), Inventory.from_string('80.00 USD {1.015 CAD}'))

        # Aggregation, more units.
        inv = Inventory.from_string('2 HOOL {500 USD}, 3 HOOL {520 USD}, 4 HOOL {530 USD}')
        self.assertEqual(inv.average(), Inventory.from_string('9 HOOL {520 USD}'))

    def test_currencies(self):
        inv = Inventory()
        self.assertEqual(set(), inv.currencies())

        inv = Inventory.from_string('40 USD {1.01 CAD}, 40 USD')
        self.assertEqual({'USD'}, inv.currencies())

        inv = Inventory.from_string('40 AAPL {1.01 USD}, 10 HOOL {2.02 USD}')
        self.assertEqual({'AAPL', 'HOOL'}, inv.currencies())

    def test_currency_pairs(self):
        inv = Inventory()
        self.assertEqual(set(), inv.currency_pairs())

        inv = Inventory.from_string('40 USD {1.01 CAD}, 40 USD')
        self.assertEqual(set([('USD', 'CAD'), ('USD', None)]), inv.currency_pairs())

        inv = Inventory.from_string('40 AAPL {1.01 USD}, 10 HOOL {2.02 USD}')
        self.assertEqual(set([('AAPL', 'USD'), ('HOOL', 'USD')]), inv.currency_pairs())

    def test_get_position(self):
        inv = Inventory(self.POSITIONS_ALL_KINDS)
        self.assertEqual(
            position.from_string('40.50 USD'),
            inv.get_position(Lot('USD', None, None)))
        self.assertEqual(
            position.from_string('40.50 USD {1.10 CAD}'),
            inv.get_position(Lot('USD', A('1.10 CAD'), None)))
        self.assertEqual(
            position.from_string('40.50 USD {1.10 CAD, 2012-01-01}'),
            inv.get_position(Lot('USD', A('1.10 CAD'), date(2012, 1, 1))))

    def test_add(self):
        inv = Inventory()
        inv.add_amount(A('100.00 USD'))
        self.checkAmount(inv, '100', 'USD')

        # Add some amount
        inv.add_amount(A('25.01 USD'))
        self.checkAmount(inv, '125.01', 'USD')

        # Subtract some amount.
        inv.add_amount(A('-12.73 USD'))
        self.checkAmount(inv, '112.28', 'USD')

        # Subtract some to be negative (should be allowed if no lot).
        inv.add_amount(A('-120 USD'))
        self.checkAmount(inv, '-7.72', 'USD')

        # Subtract some more.
        inv.add_amount(A('-1 USD'))
        self.checkAmount(inv, '-8.72', 'USD')

        # Add to above zero again
        inv.add_amount(A('18.72 USD'))
        self.checkAmount(inv, '10', 'USD')

    def test_add__booking(self):
        inv = Inventory()
        _, booking = inv.add_amount(A('100.00 USD'))
        self.assertEqual(Booking.CREATED, booking)

        _, booking = inv.add_amount(A('20.00 USD'))
        self.assertEqual(Booking.AUGMENTED, booking)

        _, booking = inv.add_amount(A('-20 USD'))
        self.assertEqual(Booking.REDUCED, booking)

        _, booking = inv.add_amount(A('-100 USD'))
        self.assertEqual(Booking.REDUCED, booking)

    def test_add_multi_currency(self):
        inv = Inventory()
        inv.add_amount(A('100 USD'))
        inv.add_amount(A('100 CAD'))
        self.checkAmount(inv, '100', 'USD')
        self.checkAmount(inv, '100', 'CAD')

        inv.add_amount(A('25 USD'))
        self.checkAmount(inv, '125', 'USD')
        self.checkAmount(inv, '100', 'CAD')

    def test_add_withlots(self):
        # Testing the strict case where everything matches, with only a cost.
        inv = Inventory()
        inv.add_amount(A('50 HOOL'), A('700 USD'))
        self.checkAmount(inv, '50', 'HOOL')

        inv.add_amount(A('-40 HOOL'), A('700 USD'))
        self.checkAmount(inv, '10', 'HOOL')

        position_, _ = inv.add_amount(A('-12 HOOL'),
                                      A('700 USD'))
        self.assertTrue(position_.is_negative_at_cost())

        # Testing the strict case where everything matches, a cost and a lot-date.
        inv = Inventory()
        inv.add_amount(A('50 HOOL'), A('700 USD'), date(2000, 1, 1))
        self.checkAmount(inv, '50', 'HOOL')

        inv.add_amount(A('-40 HOOL'), A('700 USD'), date(2000, 1, 1))
        self.checkAmount(inv, '10', 'HOOL')

        position_, _ = inv.add_amount(A('-12 HOOL'), A('700 USD'),
                                      date(2000, 1, 1))
        self.assertTrue(position_.is_negative_at_cost())

    def test_add_allow_negative(self):

        def check_allow_negative(inv):
            position_, _ = inv.add_amount(A('-11 USD'))
            self.assertFalse(position_.is_negative_at_cost())
            position_, _ = inv.add_amount(A('-11 USD'), A('1.10 CAD'))
            self.assertTrue(position_.is_negative_at_cost())
            position_, _ = inv.add_amount(A('-11 USD'), None, date(2012, 1, 1))
            self.assertTrue(position_.is_negative_at_cost())
            inv.add_amount(A('-11 USD'), A('1.10 CAD'))
            inv.add_amount(A('-11 USD'), None, date(2012, 1, 1))

        # Test adding to a position that does not exist.
        inv = Inventory()
        check_allow_negative(inv)

        # Test adding to a position that does exist.
        inv = Inventory.from_string(
            '10 USD, 10 USD {1.10 CAD}, 10 USD {1.10 CAD, 2012-01-01}')
        check_allow_negative(inv)

    def test_add_position(self):
        inv = Inventory()
        for pos in self.POSITIONS_ALL_KINDS:
            inv.add_position(pos)
        self.assertEqual(Inventory(self.POSITIONS_ALL_KINDS), inv)

    def test_op_add(self):
        inv1 = Inventory.from_string('17.00 USD')
        orig_inv1 = Inventory.from_string('17.00 USD')
        inv2 = Inventory.from_string('21.00 CAD')
        inv3 = inv1 + inv2
        self.assertEqual(Inventory.from_string('17.00 USD, 21.00 CAD'), inv3)
        self.assertEqual(orig_inv1, inv1)

    def test_update(self):
        inv1 = Inventory.from_string('11 USD')
        inv2 = Inventory.from_string('12 CAD')
        inv_updated = inv1.add_inventory(inv2)
        expect_updated = Inventory.from_string('11 USD, 12 CAD')
        self.assertEqual(expect_updated, inv_updated)
        self.assertEqual(expect_updated, inv1)

    def test_sum_inventories(self):
        inv1 = Inventory()
        inv1.add_amount(A('10 USD'))

        inv2 = Inventory()
        inv2.add_amount(A('20 CAD'))
        inv2.add_amount(A('55 HOOL'))

        inv1 + inv2
 def test_empty(self):
     inv = inventory.Inventory()
     wash_calculator.book_position(inv, DD('2014-02-03'),
                                   position.from_string('0 HOOL {500 USD}'))
     self.assertTrue(inv.is_empty())