コード例 #1
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))
コード例 #2
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
コード例 #3
0
def get_holdings_entries(entries, options_map):
    """Summarizes the entries to list of entries representing the final holdings..

    This list includes the latest prices entries as well. This can be used to
    load a full snapshot of holdings without including the entire history. This
    is a way of summarizing a balance sheet in a way that filters away history.

    Args:
      entries: A list of directives.
      options_map: A dict of parsed options.
    Returns:
      A string, the entries to print out.
    """

    # The entries will be created at the latest date, against an equity account.
    latest_date = entries[-1].date
    _, equity_account, _ = options.get_previous_accounts(options_map)

    # Get all the assets.
    holdings_list, _ = holdings.get_assets_holdings(entries, options_map)

    # Create synthetic entries for them.
    holdings_entries = []

    for index, holding in enumerate(holdings_list):
        meta = data.new_metadata('report_holdings_print', index)
        entry = data.Transaction(meta, latest_date, flags.FLAG_SUMMARIZE, None,
                                 "", None, None, [])

        # Convert the holding to a position.
        pos = holdings.holding_to_position(holding)
        entry.postings.append(
            data.Posting(holding.account, pos.units, pos.cost, None, None,
                         None))

        cost = -convert.get_cost(pos)
        entry.postings.append(
            data.Posting(equity_account, cost, None, None, None, None))

        holdings_entries.append(entry)

    # Get opening directives for all the accounts.
    used_accounts = {holding.account for holding in holdings_list}
    open_entries = summarize.get_open_entries(entries, latest_date)
    used_open_entries = [
        open_entry for open_entry in open_entries
        if open_entry.account in used_accounts
    ]

    # Add an entry for the equity account we're using.
    meta = data.new_metadata('report_holdings_print', -1)
    used_open_entries.insert(
        0, data.Open(meta, latest_date, equity_account, None, None))

    # Get the latest price entries.
    price_entries = prices.get_last_price_entries(entries, None)

    return used_open_entries + holdings_entries + price_entries
コード例 #4
0
def create_entries_from_balances(balances, date, source_account, direction,
                                 meta, flag, narration_template):
    """"Create a list of entries from a dict of balances.

    This method creates a list of new entries to transfer the amounts in the
    'balances' dict to/from another account specified in 'source_account'.

    The balancing posting is created with the equivalent at cost. In other
    words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a
    posting with this position on one leg, and with 5000 USD on the
    'source_account' leg.

    Args:
      balances: A dict of account name strings to Inventory instances.
      date: A datetime.date object, the date at which to create the transaction.
      source_account: A string, the name of the account to pull the balances
        from. This is the magician's hat to pull the rabbit from.
      direction: If 'direction' is True, the new entries transfer TO the
        balances account from the source account; otherwise the new entries
        transfer FROM the balances into the source account.
      meta: A dict to use as metadata for the transactions.
      flag: A string, the flag to use for the transactinos.
      narration_template: A format string for creating the narration. It is
        formatted with 'account' and 'date' replacement variables.
    Returns:
      A list of newly synthesizes Transaction entries.
    """
    new_entries = []
    for account, account_balance in sorted(balances.items()):

        # Don't create new entries where there is no balance.
        if account_balance.is_empty():
            continue

        narration = narration_template.format(account=account, date=date)

        if not direction:
            account_balance = -account_balance

        postings = []
        new_entry = Transaction(meta, date, flag, None, narration,
                                data.EMPTY_SET, data.EMPTY_SET, postings)

        for position in account_balance.get_positions():
            postings.append(
                data.Posting(account, position.units, position.cost, None,
                             None, None))
            cost = -convert.get_cost(position)
            postings.append(
                data.Posting(source_account, cost, None, None, None, None))

        new_entries.append(new_entry)

    return new_entries
コード例 #5
0
def get_neutralizing_postings(curmap, base_account, new_accounts):
    """Process an entry.

    Args:
      curmap: A dict of currency to a list of Postings of this transaction.
      base_account: A string, the root account name to insert.
      new_accounts: A set, a mutable accumulator of new account names.
    Returns:
      A modified entry, with new postings inserted to rebalance currency trading
      accounts.
    """
    new_postings = []
    for currency, postings in curmap.items():
        # Compute the per-currency balance.
        inv = inventory.Inventory()
        for posting in postings:
            inv.add_amount(convert.get_cost(posting))
        if inv.is_empty():
            new_postings.extend(postings)
            continue

        # Re-insert original postings and remove price conversions.
        #
        # Note: This may cause problems if the implicit_prices plugin is
        # configured to run after this one, or if you need the price annotations
        # for some scripting or serious work.
        #
        # FIXME: We need to handle these important cases (they're not frivolous,
        # this is a prototype), probably by inserting some exceptions with
        # collaborating code in the booking (e.g. insert some metadata that
        # disables price conversions on those postings).
        #
        # FIXME(2): Ouch! Some of the residual seeps through here, where there
        # are more than a single currency block. This needs fixing too. You can
        # easily mitigate some of this to some extent, by excluding transactions
        # which don't have any price conversion in them.
        for pos in postings:
            if pos.price is not None:
                pos = pos._replace(price=None)
            new_postings.append(pos)

        # Insert the currency trading accounts postings.
        amount = inv.get_only_position().units
        acc = account.join(base_account, currency)
        new_accounts.add(acc)
        new_postings.append(Posting(acc, -amount, None, None, None, None))

    return new_postings
コード例 #6
0
ファイル: inventory.py プロジェクト: rainsunny/beancount
    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
コード例 #7
0
def fill_account(entries, unused_options_map, insert_account):
    """Insert an posting with a default account when there is only a single posting.

    Args:
      entries: A list of directives.
      unused_options_map: A parser options dict.
      insert_account: A string, the name of the account.
    Returns:
      A list of entries, possibly with more Price entries than before, and a
      list of errors.
    """
    if not account.is_valid(insert_account):
        return entries, [
            FillAccountError(
                data.new_metadata('<fill_account>', 0),
                "Invalid account name: '{}'".format(insert_account), None)
        ]

    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction) and len(entry.postings) == 1:
            inv = inventory.Inventory()
            for posting in entry.postings:
                if posting.cost is None:
                    inv.add_amount(posting.units)
                else:
                    inv.add_amount(convert.get_cost(posting))
            inv.reduce(convert.get_units)
            new_postings = list(entry.postings)
            for pos in inv:
                new_postings.append(
                    data.Posting(insert_account, -pos.units, None, None, None,
                                 None))
            entry = entry._replace(postings=new_postings)
        new_entries.append(entry)

    return new_entries, []
コード例 #8
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))
コード例 #9
0
ファイル: query_env.py プロジェクト: beancount/beanquery
def position_cost(pos):
    """Get the cost of a position."""
    return convert.get_cost(pos)
コード例 #10
0
 def test_cost__missing(self):
     self.assertEqual(
         A("100 HOOL"),
         convert.get_cost(
             self._pos(A("100 HOOL"), Cost(MISSING, "USD", None, None))))
コード例 #11
0
 def test_cost__not_empty(self):
     self.assertEqual(
         A("51400.00 USD"),
         convert.get_cost(
             self._pos(A("100 HOOL"), Cost(D("514.00"), "USD", None,
                                           None))))
コード例 #12
0
 def test_cost__empty(self):
     self.assertEqual(A("100 HOOL"),
                      convert.get_cost(self._pos(A("100 HOOL"), None)))
コード例 #13
0
 def __call__(self, context):
     args = self.eval_args(context)
     return convert.get_cost(args[0])
コード例 #14
0
def get_incomplete_postings(entry, options_map):
    """Balance an entry with auto-postings and return an updated list of completed postings.

    Returns a new list of balanced postings, with the incomplete postings
    replaced with completed ones. This is probably the only place where there
    is a bit of non-trivial logic in this entire project (and the rewrite was
    to make sure it was *that* simple.)

    Note that inferred postings are tagged via metatada with an '__automatic__'
    field added to them with a true boolean value.

    Note: The 'postings' parameter may be modified or destroyed for performance
    reasons; don't reuse it.

    Args:
      entry: An instance of a valid directive.
      options_map: A dict of options, as produced by the parser.
    Returns:
      A tuple of:
        postings: a list of new postings to replace the entry's unbalanced
          postings.
        inserted: A boolean set to true if we've inserted new postings.
        errors: A list of balance errors generated during the balancing process.
        residual: A Inventory instance, the residual amounts after balancing
          the postings.
        tolerances: The tolerances inferred in the process, using the postings
          provided.
    """
    # Make a copy of the original list of postings.
    postings = list(entry.postings)

    # Errors during balancing.
    balance_errors = []

    # The list of postings without and with an explicit position.
    auto_postings_indices = []

    # Currencies seen in complete postings.
    currencies = set()

    # An inventory to accumulate the residual balance.
    residual = Inventory()

    # A dict of values for default tolerances.
    tolerances = interpolate.infer_tolerances(postings, options_map)

    # Process all the postings.
    has_nonzero_amount = False
    has_regular_postings = False
    for i, posting in enumerate(postings):
        units = posting.units
        if units is MISSING or units is None:
            # This posting will have to get auto-completed.
            auto_postings_indices.append(i)
        else:
            currencies.add(units.currency)

            # Compute the amount to balance and update the inventory.
            weight = convert.get_weight(posting)
            residual.add_amount(weight)

            has_regular_postings = True
            if weight:
                has_nonzero_amount = True

    # If there are auto-postings, fill them in.
    has_inserted = False
    if auto_postings_indices:
        # If there are too many such postings, we can't do anything, barf.
        if len(auto_postings_indices) > 1:
            balance_errors.append(
                SimpleBookingError(entry.meta,
                                   "Too many auto-postings; cannot fill in",
                                   entry))
            # Delete the redundant auto-postings.
            for index in sorted(auto_postings_indices[1:], reverse=1):
                del postings[index]

        index = auto_postings_indices[0]
        old_posting = postings[index]
        assert old_posting.price is None

        residual_positions = residual.get_positions()

        # If there are no residual positions, we want to still insert a posting
        # but with a zero position for each currency, so that the posting shows
        # up anyhow. We insert one such posting for each currency seen in the
        # complete postings. Note: if all the non-auto postings are zero, we
        # want to avoid sending a warning; the input text clearly implies the
        # author knows this would be useless.
        new_postings = []
        if not residual_positions and (
            (has_regular_postings and has_nonzero_amount)
                or not has_regular_postings):
            balance_errors.append(
                SimpleBookingError(entry.meta,
                                   "Useless auto-posting: {}".format(residual),
                                   entry))
            for currency in currencies:
                units = Amount(ZERO, currency)
                meta = copy.copy(old_posting.meta) if old_posting.meta else {}
                meta[interpolate.AUTOMATIC_META] = True
                new_postings.append(
                    Posting(old_posting.account, units, None, None,
                            old_posting.flag, old_posting.meta))
                has_inserted = True
        else:
            # Convert all the residual positions in inventory into a posting for
            # each position.
            for pos in residual_positions:
                pos = -pos

                units = pos.units
                new_units = Amount(
                    interpolate.quantize_with_tolerance(
                        tolerances, units.currency, units.number),
                    units.currency)

                meta = copy.copy(old_posting.meta) if old_posting.meta else {}
                meta[interpolate.AUTOMATIC_META] = True
                new_postings.append(
                    Posting(old_posting.account, new_units, pos.cost, None,
                            old_posting.flag, meta))
                has_inserted = True

                # Note/FIXME: This is dumb; refactor cost computation so we can
                # reuse it directly.
                new_pos = Position(new_units, pos.cost)

                # Update the residuals inventory.
                weight = convert.get_cost(new_pos)
                residual.add_amount(weight)

        postings[index:index + 1] = new_postings

    else:
        # Checking for unbalancing transactions has been moved to the validation
        # stage, so although we already have the input transaction's residuals
        # conveniently precomputed here, we are postponing the check to allow
        # plugins to "fixup" unbalancing transactions. We want to allow users to
        # be able to input unbalancing transactions as long as the final
        # transactions objects that appear on the stream (after processing the
        # plugins) are balanced. See {9e6c14b51a59}.
        pass

    return (postings, has_inserted, balance_errors, residual, tolerances)