Exemplo n.º 1
0
    def test_average(self):
        # Identity, no aggregation.
        inv = I('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 = I('40 USD {1.01 CAD}, 40 USD')
        self.assertEqual(inv.average(), inv)

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

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

        # Average on zero amount, same costs
        inv = I('2 HOOL {500 USD}')
        inv.add_amount(A('-2 HOOL'), Cost(D('500'), 'USD', None, None))
        self.assertEqual(inv.average(), I(''))

        # Average on zero amount, different costs
        inv = I('2 HOOL {500 USD}')
        inv.add_amount(A('-2 HOOL'),
                       Cost(D('500'), 'USD', datetime.date(2000, 1, 1), None))
        self.assertEqual(inv.average(), I(''))
Exemplo n.º 2
0
def test_aggregate_holdings_list_with_price():
    holdings = [
        Posting('Assets', A('10 HOOL'), None, None, None, None),
        Posting('Assets', A('14 TEST'), Cost(D('10'), 'HOOL', None, None),
                D('12'), None, None),
    ]
    assert aggregate_holdings_list(holdings) == \
        Posting('Assets', Amount(D('24'), '*'),
                Cost(D('6.25'), 'HOOL', None, None),
                D('178') / 24, None, None)
Exemplo n.º 3
0
    def test_eq_and_sortkey__bycost(self):
        pos1 = Position(A("1 USD"), None)
        pos2 = Position(A("1 USD"), Cost(D('10'), 'USD', None, None))
        pos3 = Position(A("1 USD"), Cost(D('11'), 'USD', None, None))
        pos4 = Position(A("1 USD"), Cost(D('12'), 'USD', None, None))

        positions = [pos3, pos2, pos1, pos4]
        self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))

        for _ in range(64):
            random.shuffle(positions)
            self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))
Exemplo n.º 4
0
def create_simple_posting_with_cost(entry, account, number, currency,
                                    cost_number, cost_currency):
    """Create a simple posting on the entry, with just a number and currency (no cost).

    Args:
      entry: The entry instance to add the posting to.
      account: A string, the account to use on the posting.
      number: A Decimal number or string to use in the posting's Amount.
      currency: A string, the currency for the Amount.
      cost_number: A Decimal number or string to use for the posting's cost Amount.
      cost_currency: a string, the currency for the cost Amount.
    Returns:
      An instance of Posting, and as a side-effect the entry has had its list of
      postings modified with the new Posting instance.
    """
    if isinstance(account, str):
        pass
    if not isinstance(number, Decimal):
        number = D(number)
    if cost_number and not isinstance(cost_number, Decimal):
        cost_number = D(cost_number)
    units = Amount(number, currency)
    cost = Cost(cost_number, cost_currency, None, None)
    posting = Posting(account, units, cost, None, None, None)
    if entry is not None:
        entry.postings.append(posting)
    return posting
Exemplo n.º 5
0
 def test_currency_pair(self):
     self.assertEqual(("USD", None),
                      Position(A('100 USD'), None).currency_pair())
     self.assertEqual(("AAPL", "USD"),
                      Position(A('100 AAPL'),
                               Cost('43.23', 'USD', None,
                                    None)).currency_pair())
Exemplo n.º 6
0
def convert_costspec_to_cost(posting):
    """Convert an instance of CostSpec to Cost, if present on the posting.

    If the posting has no cost, it itself is just returned.

    Args:
      posting: An instance of Posting.
    Returns:
      An instance of Posting with a possibly replaced 'cost' attribute.
    """
    cost = posting.cost
    if isinstance(cost, position.CostSpec):
        if cost is not None:
            number_per = cost.number_per
            number_total = cost.number_total
            if number_total is not None:
                # Compute the per-unit cost if there is some total cost
                # component involved.
                units_number = abs(posting.units.number)
                cost_total = number_total
                if number_per is not MISSING:
                    cost_total += number_per * units_number
                unit_cost = cost_total / units_number
            else:
                unit_cost = number_per
            new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label)
            posting = posting._replace(units=posting.units, cost=new_cost)
    return posting
Exemplo n.º 7
0
    def test_from_amounts(self):
        pos = from_amounts(A('10.00 USD'))
        self.assertEqual(Position(A("10 USD")), pos)

        pos = from_amounts(A('10 HOOL'), A('510.00 USD'))
        self.assertEqual(
            Position(A("10 HOOL"), Cost(D('510'), 'USD', None, None)), pos)
Exemplo n.º 8
0
def convert_spec_to_cost(units, cost_spec):
    """Convert a posting's CostSpec instance to a Cost.

    Args:
      units: An instance of Amount.
      cost_spec: An instance of CostSpec.
    Returns:
      An instance of Cost.
    """
    cost = cost_spec
    errors = []
    if isinstance(units, Amount):
        currency = units.currency
        if cost_spec is not None:
            number_per, number_total, cost_currency, date, label, merge = cost_spec

            # Compute the cost.
            if number_per is not MISSING or number_total is not None:
                if number_total is not None:
                    # Compute the per-unit cost if there is some total cost
                    # component involved.
                    units_num = units.number
                    cost_total = number_total
                    if number_per is not MISSING:
                        cost_total += number_per * units_num
                    unit_cost = cost_total / abs(units_num)
                else:
                    unit_cost = number_per
                cost = Cost(unit_cost, cost_currency, date, label)
            else:
                cost = None
    return cost
Exemplo n.º 9
0
 def test_units(self):
     units = A("100 HOOL")
     self.assertEqual(
         units,
         convert.get_units(
             self._pos(units, Cost(D("514.00"), "USD", None, None))))
     self.assertEqual(units, convert.get_units(self._pos(units, None)))
Exemplo n.º 10
0
def test_holdings_with_prices(load_doc):
    """
    2013-04-05 *
      Equity:Unknown
      Assets:Cash			50000 USD

    2013-04-01 *
      Assets:Account1             15 HOOL {518.73 USD}
      Assets:Cash

    2013-06-01 price HOOL  578.02 USD

    """
    entries, _, _ = load_doc
    price_map = prices.build_price_map(entries)

    holdings = get_final_holdings(entries, ('Assets'), price_map)
    holdings = sorted(map(tuple, holdings))

    assert holdings == [
        ('Assets:Account1', A('15 HOOL'),
         Cost(D('518.73'), 'USD', datetime.date(2013, 4, 1),
              None), D('578.02'), None, None),
        ('Assets:Cash', A('42219.05 USD'), None, None, None, None),
    ]
Exemplo n.º 11
0
    def average(self):
        """Average all lots of the same currency together.

        Returns:
          An instance of Inventory.
        """
        groups = collections.defaultdict(list)
        for position in self:
            key = (position.units.currency,
                   position.cost.currency if position.cost else None)
            groups[key].append(position)

        average_inventory = Inventory()
        for (currency, cost_currency), positions in groups.items():
            total_units = sum(position.units.number for position in positions)
            units_amount = Amount(total_units, currency)

            if cost_currency:
                total_cost = sum(
                    convert.get_cost(position).number
                    for position in positions)
                cost = Cost(total_cost / total_units, cost_currency, None,
                            None)
            else:
                cost = None

            average_inventory.add_amount(units_amount, cost)

        return average_inventory
Exemplo n.º 12
0
    def test_quantities(self):
        pos = Position(A("10 USD"), None)
        self.assertEqual(A('10 USD'), pos.units)
        self.assertEqual(A('10 USD'), convert.get_cost(pos))

        pos = Position(A("10 USD"), Cost(D('1.5'), 'AUD', None, None))
        self.assertEqual(A('10 USD'), pos.units)
        self.assertEqual(A('15 AUD'), convert.get_cost(pos))
Exemplo n.º 13
0
    def test_from_string(self):
        inv = inventory.from_string('')
        self.assertEqual(Inventory(), inv)

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

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

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

        inv = inventory.from_string('2.2 HOOL {532.43 USD}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(A("2.2 HOOL"), Cost(D('532.43'), 'USD', None, None)),
                       Position(A("3.413 EUR"))]),
            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(A("2.2 HOOL"),
                                Cost(D('532.43'), 'USD', None, None)),
                       Position(A("2.3 HOOL"),
                                Cost(D('564.00'), 'USD', datetime.date(2015, 7, 14), None)),
                       Position(A("3.413 EUR"))]),
            inv)

        inv = inventory.from_string(
            '1.1 HOOL {500.00 # 11.00 USD}, 100 CAD')
        self.assertEqual(
            Inventory([Position(A("1.1 HOOL"),
                                Cost(D('510.00'), 'USD', None, None)),
                       Position(A("100 CAD"))]),
            inv)
Exemplo n.º 14
0
    def test_add_amount__withlots(self):
        # Testing the strict case where everything matches, with only a cost.
        inv = Inventory()
        inv.add_amount(A('50 HOOL'), Cost(D('700'), 'USD', None, None))
        self.checkAmount(inv, '50', 'HOOL')

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

        position_, _ = inv.add_amount(A('-12 HOOL'),
                                      Cost(D('700'), 'USD', None, None))
        self.assertTrue(next(iter(inv)).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'),
                       Cost(D('700'), 'USD', date(2000, 1, 1), None))
        self.checkAmount(inv, '50', 'HOOL')

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

        position_, _ = inv.add_amount(
            A('-12 HOOL'), Cost(D('700'), 'USD', date(2000, 1, 1), None))
        self.assertTrue(next(iter(inv)).is_negative_at_cost())
Exemplo n.º 15
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}'))
Exemplo n.º 16
0
def create_simple_posting_with_cost_or_price(entry,
                                             account,
                                             number,
                                             currency,
                                             price_number=None,
                                             price_currency=None,
                                             cost_number=None,
                                             cost_currency=None,
                                             costspec=None):
    """Create a simple posting on the entry, with a cost (for purchases) or price (for sell transactions).

    Args:
      entry: The entry instance to add the posting to.
      account: A string, the account to use on the posting.
      number: A Decimal number or string to use in the posting's Amount.
      currency: A string, the currency for the Amount.
      price_number: A Decimal number or string to use for the posting's price Amount.
      price_currency: a string, the currency for the price Amount.
      cost_number: A Decimal number or string to use for the posting's cost Amount.
      cost_currency: a string, the currency for the cost Amount.
    Returns:
      An instance of Posting, and as a side-effect the entry has had its list of
      postings modified with the new Posting instance.
    """
    if isinstance(account, str):
        pass
    if not isinstance(number, Decimal):
        number = D(number)
    units = Amount(number, currency)

    if not (price_number or cost_number):
        print("Either price ({}) or cost ({}) must be specified ({})".format(
            price_number, cost_number, entry))
        import pdb
        pdb.set_trace()
        raise Exception(
            "Either price ({}) or cost ({}) must be specified".format(
                price_number, cost_number))

    price = Amount(price_number, price_currency) if price_number else None
    cost = Cost(cost_number, cost_currency, None,
                None) if cost_number else None
    cost = costspec if costspec else cost
    posting = data.Posting(account, units, cost, price, None, None)

    if entry is not None:
        entry.postings.append(posting)
    return posting
Exemplo n.º 17
0
    def average(self):
        """Average all lots of the same currency together.

        Use the minimum date from each aggregated set of lots.

        Returns:
          An instance of Inventory.
        """
        groups = collections.defaultdict(list)
        for position in self:
            key = (position.units.currency,
                   position.cost.currency if position.cost else None)
            groups[key].append(position)

        average_inventory = Inventory()
        for (currency, cost_currency), positions in groups.items():
            total_units = sum(position.units.number
                              for position in positions)
            # Explicitly skip aggregates when resulting in zero units.
            if total_units == ZERO:
                continue
            units_amount = Amount(total_units, currency)

            if cost_currency:
                total_cost = sum(convert.get_cost(position).number
                                 for position in positions)
                cost_number = (Decimal('Infinity')
                               if total_units == ZERO
                               else (total_cost / total_units))
                min_date = None
                for pos in positions:
                    pos_date = pos.cost.date if pos.cost else None
                    if pos_date is not None:
                        min_date = (pos_date
                                    if min_date is None
                                    else min(min_date, pos_date))
                cost = Cost(cost_number, cost_currency, min_date, None)
            else:
                cost = None

            average_inventory.add_amount(units_amount, cost)

        return average_inventory
Exemplo n.º 18
0
def aggregate_holdings_list(holdings):
    if not holdings:
        return None

    units, total_book_value, total_market_value = ZERO, ZERO, ZERO
    accounts = set()
    currencies = set()
    cost_currencies = set()
    for pos in holdings:
        units += pos.units.number
        accounts.add(pos.account)
        currencies.add(pos.units.currency)
        cost_currencies.add(
            pos.cost.currency if pos.cost else pos.units.currency)

        if pos.cost:
            total_book_value += pos.units.number * pos.cost.number
        else:
            total_book_value += pos.units.number

        if pos.price is not None:
            total_market_value += pos.units.number * pos.price
        else:
            total_market_value += \
                pos.units.number * (pos.cost.number if pos.cost else 1)

    assert len(cost_currencies) == 1

    avg_cost = total_book_value / units if units else None
    avg_price = total_market_value / units if units else None

    currency = currencies.pop() if len(currencies) == 1 else '*'
    cost_currency = cost_currencies.pop()
    account_ = (accounts.pop()
                if len(accounts) == 1 else account.commonprefix(accounts))
    show_cost = bool(avg_cost) and cost_currency != currency

    return Posting(
        account_, Amount(units, currency),
        Cost(avg_cost, cost_currency, None, None) if show_cost else None,
        avg_price if show_cost else None, None, None)
Exemplo n.º 19
0
                'amount': ''
            },
        ],
    }

    with app.test_request_context():
        serialised = loads(dumps(serialise(txn)))
    assert serialised == json_txn


@pytest.mark.parametrize(
    'pos,amount',
    [
        ((A('100 USD'), None, None, None, None), '100 USD'),
        (
            (A('100 USD'), Cost(D('10'), 'EUR', None, None), None, None, None),
            '100 USD {10 EUR}',
        ),
        (
            (
                A('100 USD'),
                Cost(D('10'), 'EUR', None, None),
                A('11 EUR'),
                None,
                None,
            ),
            '100 USD {10 EUR} @ 11 EUR',
        ),
        ((A('100 USD'), None, A('11 EUR'), None, None), '100 USD @ 11 EUR'),
        (
            (
Exemplo n.º 20
0
 def test_from_string__with_label(self):
     pos = from_string('2.2 HOOL {"78c3f7f1315b"}')
     self.assertEqual(
         Position(A("2.2 HOOL"), Cost(None, None, None, "78c3f7f1315b")),
         pos)
Exemplo n.º 21
0
 def test_from_string__with_compound_cost(self):
     pos = from_string('1.1 HOOL {500.00 # 11.00 USD}')
     self.assertEqual(
         Position(A("1.1 HOOL"), Cost(D('510.00'), 'USD', None, None)), pos)
Exemplo n.º 22
0
 def test_from_string__with_everything(self):
     pos = from_string(
         '20 HOOL {532.43 # 20.00 USD, "e4dc1a361022", 2014-06-15}')
     cost = Cost(D('533.43'), 'USD', datetime.date(2014, 6, 15),
                 "e4dc1a361022")
     self.assertEqual(Position(A("20 HOOL"), cost), pos)
Exemplo n.º 23
0
 def test_constructors(self):
     Position(A('123.45 USD'), None)
     Position(A('123.45 USD'), Cost('74.00', 'CAD', None, None))
     Position(A('123.45 USD'), Cost('74.00', 'CAD', date(2013, 2, 3), None))
     Position(Amount(D('123.45'), None), None)
     Position(Amount(None, 'USD'), None)
Exemplo n.º 24
0
 def test_negative(self):
     pos = Position(A("28372 USD"), Cost(D('10'), 'AUD', None, None))
     negpos = pos.get_negative()
     self.assertEqual(A('-28372 USD'), negpos.units)
     self.assertEqual(A('-283720 AUD'), convert.get_cost(negpos))
Exemplo n.º 25
0
def test_get_final_holdings(load_doc):
    """
    2013-04-05 *
      Equity:Unknown
      Assets:Cash			50000 USD

    2013-04-01 *
      Assets:Account1             15 HOOL {518.73 USD}
      Assets:Cash

    2013-04-02 *
      Assets:Account1             10 HOOL {523.46 USD}
      Assets:Cash

    2013-04-03 *
      Assets:Account1             -4 HOOL {518.73 USD}
      Assets:Cash

    2013-04-02 *
      Assets:Account2            20 ITOT {85.195 USD}
      Assets:Cash

    2013-04-03 *
      Assets:Account3             50 HOOL {540.00 USD} @ 560.00 USD
      Assets:Cash

    2013-04-10 *
      Assets:Cash			5111 USD
      Liabilities:Loan
    """
    entries, _, _ = load_doc

    holdings = get_final_holdings(entries)
    holdings = sorted(map(tuple, holdings))

    assert holdings == [
        ('Assets:Account1', A('10 HOOL'), Cost(D('523.46'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account1', A('11 HOOL'), Cost(D('518.73'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account2', A('20 ITOT'), Cost(D('85.195'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account3', A('50 HOOL'), Cost(D('540.00'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Cash', A('15466.470 USD'), None, None, None, None),
        ('Equity:Unknown', A('-50000 USD'), None, None, None, None),
        ('Liabilities:Loan', A('-5111 USD'), None, None, None, None),
    ]

    holdings = get_final_holdings(entries, ('Assets'))
    holdings = sorted(map(tuple, holdings))

    assert holdings == [
        ('Assets:Account1', A('10 HOOL'), Cost(D('523.46'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account1', A('11 HOOL'), Cost(D('518.73'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account2', A('20 ITOT'), Cost(D('85.195'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Account3', A('50 HOOL'), Cost(D('540.00'), 'USD', None,
                                               None), None, None, None),
        ('Assets:Cash', A('15466.470 USD'), None, None, None, None),
    ]
Exemplo n.º 26
0
 def test_is_negative_at_cost(self):
     pos1 = Position(A("1 USD"), Cost('10', 'AUD', None, None))
     pos2 = Position(A("-1 USD"), Cost('10', 'AUD', None, None))
     self.assertFalse(pos1.is_negative_at_cost())
     self.assertTrue(pos2.is_negative_at_cost())
Exemplo n.º 27
0
def oneliner(entries, options_map, config):
  """Parse note oneliners into valid transactions. For example,
  1999-12-31 note Assets:Cash "Income:Test -16.18 EUR * Description goes here *" """

  errors = []

  new_entries = []

  for entry in entries:
    if(isinstance(entry, data.Note) and entry.comment[-1:] == "*"):
      comment = entry.comment
      try:
        k = None
        maybe_cost = RE_COST.findall(comment)
        if len(maybe_cost) > 0:
          amount = maybe_cost[0].split()[0]
          currency = maybe_cost[0].split()[1]
          cost = Cost(D(amount), currency, None, None)
          k = mul(cost, D(-1))
          comment = RE_COST.sub('', comment)
        else:
          cost = None

        maybe_price = RE_PRICE.findall(comment)
        if len(maybe_price) > 0:
          price = Amount.from_string(maybe_price[0])
          k = k or mul(price, D(-1))
          comment = RE_PRICE.sub('', comment)
        else:
          price = None

        comment_tuple = comment.split()
        other_account = comment_tuple[0]
        units = Amount.from_string(' '.join(comment_tuple[1:3]))
        flag = comment_tuple[3]
        narration_tmp = ' '.join(comment_tuple[4:-1])
        tags = {'NoteToTx'}
        for tag in RE_TAG.findall(narration_tmp):
          tags.add( tag[1] )
        narration = RE_TAG.sub('', narration_tmp).rstrip()

        k = k or Amount(D(-1), units.currency)

        # print(type(cost), cost, type(price), price, type(units), units, k, comment)
        p1 = data.Posting(account=other_account,
                  units=units,
                  cost=cost,
                  price=price,
                  flag=None,
                  meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']})
        p2 = data.Posting(account=entry.account,
                  units=mul(k, units.number),
                  cost=cost,
                  price=None,
                  flag=None,
                  meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']})
        e = data.Transaction(date=entry.date,
                   flag=flag,
                   payee=None,  # TODO
                   narration=narration,
                   tags=tags,  # TODO
                   links=EMPTY_SET,  # TODO
                   postings=[p1, p2],
                   meta=entry.meta)
        new_entries.append(e)
        # print(e)
      except:
        print('beancount_oneliner error:', entry, sys.exc_info())
    else:
      new_entries.append(entry)

  return new_entries, errors
Exemplo n.º 28
0
def booking_method_AVERAGE(entry, posting, matches):
    """AVERAGE booking method implementation."""
    booked_reductions = []
    booked_matches = []
    errors = [AmbiguousMatchError(entry.meta, "AVERAGE method is not supported", entry)]
    return booked_reductions, booked_matches, errors, False

    # FIXME: Future implementation here.
    # pylint: disable=unreachable
    if False: # pylint: disable=using-constant-test
        # DISABLED - This is the code for AVERAGE, which is currently disabled.

        # If there is more than a single match we need to ultimately merge the
        # postings. Also, if the reducing posting provides a specific cost, we
        # need to update the cost basis as well. Both of these cases are carried
        # out by removing all the matches and readding them later on.
        if len(matches) == 1 and (
                not isinstance(posting.cost.number_per, Decimal) and
                not isinstance(posting.cost.number_total, Decimal)):
            # There is no cost. Just reduce the one leg. This should be the
            # normal case if we always merge augmentations and the user lets
            # Beancount deal with the cost.
            match = matches[0]
            sign = -1 if posting.units.number < ZERO else 1
            number = min(abs(match.units.number), abs(posting.units.number))
            match_units = Amount(number * sign, match.units.currency)
            booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
            insufficient = (match_units.number != posting.units.number)
        else:
            # Merge the matching postings to a single one.
            merged_units = inventory.Inventory()
            merged_cost = inventory.Inventory()
            for match in matches:
                merged_units.add_amount(match.units)
                merged_cost.add_amount(convert.get_weight(match))
            if len(merged_units) != 1 or len(merged_cost) != 1:
                errors.append(
                    AmbiguousMatchError(
                        entry.meta,
                        'Cannot merge positions in multiple currencies: {}'.format(
                            ', '.join(position.to_string(match_posting)
                                      for match_posting in matches)), entry))
            else:
                if (isinstance(posting.cost.number_per, Decimal) or
                    isinstance(posting.cost.number_total, Decimal)):
                    errors.append(
                        AmbiguousMatchError(
                            entry.meta,
                            "Explicit cost reductions aren't supported yet: {}".format(
                                position.to_string(posting)), entry))
                else:
                    # Insert postings to remove all the matches.
                    booked_reductions.extend(
                        posting._replace(units=-match.units, cost=match.cost,
                                         flag=flags.FLAG_MERGING)
                        for match in matches)
                    units = merged_units[0].units
                    date = matches[0].cost.date  ## FIXME: Select which one,
                                                 ## oldest or latest.
                    cost_units = merged_cost[0].units
                    cost = Cost(cost_units.number/units.number, cost_units.currency,
                                date, None)

                    # Insert a posting to refill those with a replacement match.
                    booked_reductions.append(
                        posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING))

                    # Now, match the reducing request against this lot.
                    booked_reductions.append(
                        posting._replace(units=posting.units, cost=cost))
                    insufficient = abs(posting.units.number) > abs(units.number)
    def _make_import_result(self, x) -> ImportResult:
        if x._fields == DepositShares._fields:
            deposit = DepositShares(*x)
            cash_basis = Amount(number=round_to(
                deposit.vest_fmv.number * deposit.shares, D("0.001")),
                                currency=deposit.vest_fmv.currency)
            transaction = Transaction(
                meta=None,
                date=deposit.date,
                flag=FLAG_OKAY,
                payee=None,
                narration='Deposit %s as %d %s shares' %
                (cash_basis, deposit.shares, deposit.symbol),
                tags=EMPTY_SET,
                links={deposit.award_id},
                postings=[
                    Posting(account=self.eac_account,
                            units=Amount(deposit.shares, deposit.symbol),
                            cost=Cost(number=deposit.vest_fmv.number,
                                      currency=deposit.vest_fmv.currency,
                                      date=deposit.vest_date,
                                      label=None),
                            price=None,
                            flag=None,
                            meta=collections.OrderedDict(
                                source_desc=deposit.source_desc,
                                date=deposit.date,
                                award_date=deposit.award_date,
                            )),
                    Posting(
                        account=self.stock_income_account,
                        units=-cash_basis,
                        cost=None,
                        price=None,
                        flag=None,
                        meta=None,
                    ),
                ])
        elif x._fields == JournalTransfer._fields:
            tran = JournalTransfer(*x)
            dest_account = FIXME_ACCOUNT
            narration = tran.description
            if tran.symbol in self.commodity_dest_acct:
                dest_account = self.commodity_dest_acct[tran.symbol]
            if len(tran.basis) > 0:
                postings = []
                set_meta = False
                narration = f"{tran.shares} {tran.symbol} {narration}"
                for basis in tran.basis:
                    units = Amount(basis.shares, tran.symbol)
                    meta = collections.OrderedDict()
                    if not set_meta:
                        meta = collections.OrderedDict(
                            source_desc=tran.source_desc,
                            date=tran.date,
                        )
                        set_meta = True
                    postings.append(
                        Posting(account=self.eac_account,
                                units=-units,
                                cost=Cost(basis.vest_fmv.number,
                                          basis.vest_fmv.currency,
                                          basis.vest_date, None),
                                price=None,
                                flag=None,
                                meta=meta))
                    postings.append(
                        Posting(
                            account=dest_account,
                            units=units,
                            cost=Cost(basis.vest_fmv.number,
                                      basis.vest_fmv.currency, basis.vest_date,
                                      None),
                            price=None,
                            flag=None,
                            meta=None,
                        ))
            else:
                postings = [
                    Posting(account=self.eac_account,
                            units=tran.cash,
                            cost=None,
                            price=None,
                            flag=None,
                            meta=collections.OrderedDict(
                                source_desc=tran.source_desc,
                                date=tran.date,
                            )),
                    Posting(
                        account=dest_account,
                        units=-tran.cash,
                        cost=None,
                        price=None,
                        flag=None,
                        meta=None,
                    )
                ]
            if tran.action == "Journal":
                # TODO: infer correct account from description
                dest_account = self.cash_account
            transaction = Transaction(meta=None,
                                      date=tran.date,
                                      flag=FLAG_OKAY,
                                      payee=None,
                                      narration=narration,
                                      tags=EMPTY_SET,
                                      links=EMPTY_SET,
                                      postings=postings)

        elif x._fields == Sale._fields:
            sale = Sale(*x)
            transaction = Transaction(
                meta=None,
                date=sale.date,
                flag=FLAG_OKAY,
                payee=None,
                narration=sale.description + " %s %s" %
                (sale.shares, sale.symbol),
                tags=EMPTY_SET,
                links=set(map(lambda b: b.grant_id, sale.basis)),
                postings=[
                    Posting(account=self.eac_account,
                            units=sale.cash,
                            cost=None,
                            price=None,
                            flag=None,
                            meta=collections.OrderedDict(
                                source_desc=sale.source_desc,
                                date=sale.date,
                            )),
                    Posting(
                        account=self.fees_account,
                        units=sale.fees,
                        cost=None,
                        price=None,
                        flag=None,
                        meta=None,
                    ),
                ])
            for basis in sale.basis:
                isLong = sale.date - basis.vest_date > datetime.timedelta(365)
                costbasis = basis.vest_fmv.number * basis.shares
                gain = basis.gross_proceeds.number - costbasis
                transaction.postings.append(
                    Posting(
                        account=self.eac_account,
                        units=Amount(-basis.shares, sale.symbol),
                        cost=Cost(
                            number=basis.vest_fmv.number,
                            currency=basis.vest_fmv.currency,
                            date=basis.vest_date,
                            label=None,
                        ),
                        price=basis.sale_price,
                        flag=None,
                        meta=collections.OrderedDict(basis=costbasis, ),
                    ))
                # breaking gain out per basis makes it easier to file on taxes
                pnl_suffix = ":Long" if isLong else ":Short"
                transaction.postings.append(
                    Posting(
                        account=self.pnl_account + pnl_suffix,
                        units=Amount(-gain, "USD"),
                        cost=None,
                        price=None,
                        flag=None,
                        meta=None,
                    ))
        else:
            raise RuntimeError("Invalid import: " + x)
        return ImportResult(date=x.date,
                            info=dict(type='text/csv', filename=x.file,
                                      line=1),
                            entries=[transaction])
Exemplo n.º 30
0
        txn = txn._replace(payee="")
        json_txn["payee"] = ""
        serialised = loads(dumps(serialise(txn)))
        assert serialised == json_txn

        txn = txn._replace(payee=None)
        serialised = loads(dumps(serialise(txn)))
        assert serialised == json_txn


@pytest.mark.parametrize(
    "pos,amount",
    [
        ((A("100 USD"), None, None, None, None), "100 USD"),
        (
            (A("100 USD"), Cost(D("10"), "EUR", None, None), None, None, None),
            "100 USD {10 EUR}",
        ),
        (
            (
                A("100 USD"),
                Cost(D("10"), "EUR", None, None),
                A("11 EUR"),
                None,
                None,
            ),
            "100 USD {10 EUR} @ 11 EUR",
        ),
        ((A("100 USD"), None, A("11 EUR"), None, None), "100 USD @ 11 EUR"),
        (
            (