コード例 #1
0
ファイル: salary.py プロジェクト: dragoscc/beancount-1
def main():
    parser = argparse.ArgumentParser(description=__doc__.strip())

    parser.add_argument('filename', help='Beancount filename')
    parser.add_argument('account', help='Root account to consider')

    parser.add_argument('-c',
                        '--currency',
                        action='store',
                        default='USD',
                        help="The currency to pull out of the inventory")

    args = parser.parse_args()

    # Load the Beancount input file.
    entries, _, options_map = loader.load_file(args.filename)

    # Compute monthly time intervals.
    start_date = datetime.date(2013, 1, 28)
    dateiter = iter(
        rrule.rrule(rrule.MONTHLY,
                    dtstart=datetime.datetime(2013, 1, 1),
                    until=datetime.datetime.now()))

    # Compute cumulative totals accumulated at those dates.
    curve = [(datetime.date(2013, 1, 28), Decimal())]
    date = next(dateiter).date()
    balance = inventory.Inventory()
    is_account = account.parent_matcher(args.account)
    for entry in entries:
        if entry.date >= date:
            # At the boundary, save the date and total number.
            try:
                total = -balance.get_units(args.currency).number
                curve.append((date, total))
                date = next(dateiter).date()
            except StopIteration:
                break

        # Sum up the amounts from those accounts.
        if isinstance(entry, data.Transaction):
            for posting in entry.postings:
                if is_account(posting.account):
                    balance.add_position(posting.position)

    # Compute multiple averages over fixed windows of a number of months and
    # plot them.
    months = [None, 12, 6, 3]
    for num in months:
        series = rolling_average(curve, num)
        pyplot.plot([date for date, total in series],
                    [total for date, total in series],
                    label=str(num))
        print('{:10}: {:10,.2f}'.format(num if num is not None else 0,
                                        series[-1][1]))

    # Show that joint plot.
    pyplot.legend()
    pyplot.tight_layout()
    pyplot.show()
コード例 #2
0
ファイル: account_test.py プロジェクト: simonla/beancount-1
 def test_parent_matcher(self):
     is_child = account.parent_matcher('Assets:Bank:Checking')
     self.assertTrue(is_child('Assets:Bank:Checking'))
     self.assertTrue(is_child('Assets:Bank:Checking:SubAccount'))
     self.assertFalse(is_child('Assets:Bank:CheckingOld'))
     self.assertFalse(is_child('Assets:Bank:Checking-Old'))
コード例 #3
0
ファイル: pad.py プロジェクト: dragoscc/beancount-1
def pad(entries, options_map):
    """Insert transaction entries for to fulfill a subsequent balance check.

    Synthesize and insert Transaction entries right after Pad entries in order
    to fulfill checks in the padded accounts. Returns a new list of entries.
    Note that this doesn't pad across parent-child relationships, it is a very
    simple kind of pad. (I have found this to be sufficient in practice, and
    simpler to implement and understand.)

    Furthermore, this pads for a single currency only, that is, balance checks
    are specified only for one currency at a time, and pads will only be
    inserted for those currencies.

    Args:
      entries: A list of directives.
      options_map: A parser options dict.
    Returns:
      A new list of directives, with Pad entries inserte, and a list of new
      errors produced.
    """
    pad_errors = []

    # Find all the pad entries and group them by account.
    pads = list(misc_utils.filter_type(entries, data.Pad))
    pad_dict = misc_utils.groupby(lambda x: x.account, pads)

    # Partially realize the postings, so we can iterate them by account.
    by_account = realization.postings_by_account(entries)

    # A dict of pad -> list of entries to be inserted.
    new_entries = {id(pad): [] for pad in pads}

    # Process each account that has a padding group.
    for account_, pad_list in sorted(pad_dict.items()):

        # Last encountered / currency active pad entry.
        active_pad = None

        # Gather all the postings for the account and its children.
        postings = []
        is_child = account.parent_matcher(account_)
        for item_account, item_postings in by_account.items():
            if is_child(item_account):
                postings.extend(item_postings)
        postings.sort(key=data.posting_sortkey)

        # A set of currencies already padded so far in this account.
        padded_lots = set()

        pad_balance = inventory.Inventory()
        for entry in postings:

            assert not isinstance(entry, data.Posting)
            if isinstance(entry, data.TxnPosting):
                # This is a transaction; update the running balance for this
                # account.
                pad_balance.add_position(entry.posting.position)

            elif isinstance(entry, data.Pad):
                if entry.account == account_:
                    # Mark this newly encountered pad as active and allow all lots
                    # to be padded heretofore.
                    active_pad = entry
                    padded_lots = set()

            elif isinstance(entry, data.Balance):
                check_amount = entry.amount

                # Compare the current balance amount to the expected one from
                # the check entry. IMPORTANT: You need to understand that this
                # does not check a single position, but rather checks that the
                # total amount for a particular currency (which itself is
                # distinct from the cost).
                balance_amount = pad_balance.get_units(check_amount.currency)
                diff_amount = amount.amount_sub(balance_amount, check_amount)

                # Use the specified tolerance or automatically infer it.
                tolerance = balance.get_tolerance(entry, options_map)

                if abs(diff_amount.number) > tolerance:
                    # The check fails; we need to pad.

                    # Pad only if pad entry is active and we haven't already
                    # padded that lot since it was last encountered.
                    if active_pad and (check_amount.currency
                                       not in padded_lots):

                        # Note: we decide that it's an error to try to pad
                        # positions at cost; we check here that all the existing
                        # positions with that currency have no cost.
                        positions = [
                            pos for pos in pad_balance.get_positions()
                            if pos.lot.currency == check_amount.currency
                        ]
                        for position_ in positions:
                            if position_.lot.cost is not None:
                                pad_errors.append(
                                    PadError(entry.meta, (
                                        "Attempt to pad an entry with cost for "
                                        "balance: {}".format(pad_balance)),
                                             active_pad))

                        # Thus our padding lot is without cost by default.
                        lot = position.Lot(check_amount.currency, None, None)
                        diff_position = position.Position(
                            lot, check_amount.number - balance_amount.number)

                        # Synthesize a new transaction entry for the difference.
                        narration = ('(Padding inserted for Balance of {} for '
                                     'difference {})').format(
                                         check_amount, diff_position)
                        new_entry = data.Transaction(active_pad.meta.copy(),
                                                     active_pad.date,
                                                     flags.FLAG_PADDING, None,
                                                     narration, None, None, [])

                        new_entry.postings.append(
                            data.Posting(active_pad.account, diff_position,
                                         None, None, None))
                        new_entry.postings.append(
                            data.Posting(active_pad.source_account,
                                         -diff_position, None, None, None))

                        # Save it for later insertion after the active pad.
                        new_entries[id(active_pad)].append(new_entry)

                        # Fixup the running balance.
                        position_, _ = pad_balance.add_position(diff_position)
                        if position_.is_negative_at_cost():
                            raise ValueError(
                                "Position held at cost goes negative: {}".
                                format(position_))

                # Mark this lot as padded. Further checks should not pad this lot.
                padded_lots.add(check_amount.currency)

    # Insert the newly created entries right after the pad entries that created them.
    padded_entries = []
    for entry in entries:
        padded_entries.append(entry)
        if isinstance(entry, data.Pad):
            entry_list = new_entries[id(entry)]
            if entry_list:
                padded_entries.extend(entry_list)
            else:
                # Generate errors on unused pad entries.
                pad_errors.append(
                    PadError(entry.meta, "Unused Pad entry", entry))

    return padded_entries, pad_errors
コード例 #4
0
def check(entries, options_map):
    """Process the balance assertion directives.

    For each Balance directive, check that their expected balance corresponds to
    the actual balance computed at that time and replace failing ones by new
    ones with a flag that indicates failure.

    Args:
      entries: A list of directives.
      options_map: A dict of options, parsed from the input file.
    Returns:
      A pair of a list of directives and a list of balance check errors.
    """
    new_entries = []
    check_errors = []

    # This is similar to realization, but performed in a different order, and
    # where we only accumulate inventories for accounts that have balance
    # assertions in them (this saves on time). Here we process the entries one
    # by one along with the balance checks. We use a temporary realization in
    # order to hold the incremental tree of balances, so that we can easily get
    # the amounts of an account's subaccounts for making checks on parent
    # accounts.
    real_root = realization.RealAccount('')

    # Figure out the set of accounts for which we need to compute a running
    # inventory balance.
    asserted_accounts = {
        entry.account
        for entry in entries if isinstance(entry, Balance)
    }

    # Add all children accounts of an asserted account to be calculated as well,
    # and pre-create these accounts, and only those (we're just being tight to
    # make sure).
    asserted_match_list = [
        account.parent_matcher(account_) for account_ in asserted_accounts
    ]
    for account_ in getters.get_accounts(entries):
        if (account_ in asserted_accounts
                or any(match(account_) for match in asserted_match_list)):
            realization.get_or_create(real_root, account_)

    # Get the Open directives for each account.
    open_close_map = getters.get_account_open_close(entries)

    for entry in entries:
        if isinstance(entry, Transaction):
            # For each of the postings' accounts, update the balance inventory.
            for posting in entry.postings:
                real_account = realization.get(real_root, posting.account)

                # The account will have been created only if we're meant to track it.
                if real_account is not None:
                    # Note: Always allow negative lots for the purpose of balancing.
                    # This error should show up somewhere else than here.
                    real_account.balance.add_position(posting)

        elif isinstance(entry, Balance):
            # Check that the currency of the balance check is one of the allowed
            # currencies for that account.
            expected_amount = entry.amount
            try:
                open, _ = open_close_map[entry.account]
            except KeyError:
                check_errors.append(
                    BalanceError(
                        entry.meta,
                        "Account '{}' does not exist: ".format(entry.account),
                        entry))
                continue

            if (expected_amount is not None and open and open.currencies
                    and expected_amount.currency not in open.currencies):
                check_errors.append(
                    BalanceError(
                        entry.meta,
                        "Invalid currency '{}' for Balance directive: ".format(
                            expected_amount.currency), entry))

            # Check the balance against the check entry.
            real_account = realization.get(real_root, entry.account)
            assert real_account is not None, "Missing {}".format(entry.account)

            # Sum up the current balances for this account and its
            # sub-accounts. We want to support checks for parent accounts
            # for the total sum of their subaccounts.
            subtree_balance = inventory.Inventory()
            for real_child in realization.iter_children(real_account, False):
                subtree_balance += real_child.balance

            # Get only the amount in the desired currency.
            balance_amount = subtree_balance.get_currency_units(
                expected_amount.currency)

            # Check if the amount is within bounds of the expected amount.
            diff_amount = amount.sub(balance_amount, expected_amount)

            # Use the specified tolerance or automatically infer it.
            tolerance = get_balance_tolerance(entry, options_map)

            if abs(diff_amount.number) > tolerance:
                check_errors.append(
                    BalanceError(
                        entry.meta,
                        ("Balance failed for '{}': "
                         "expected {} != accumulated {} ({} {})").format(
                             entry.account, expected_amount, balance_amount,
                             abs(diff_amount.number),
                             ('too much' if diff_amount.number > 0 else
                              'too little')), entry))

                # Substitute the entry by a failing entry, with the diff_amount
                # field set on it. I'm not entirely sure that this is the best
                # of ideas, maybe leaving the original check intact and insert a
                # new error entry might be more functional or easier to
                # understand.
                entry = entry._replace(meta=entry.meta.copy(),
                                       diff_amount=diff_amount)

        new_entries.append(entry)

    return new_entries, check_errors
コード例 #5
0
ファイル: balexpr.py プロジェクト: w1ndy/beancount_balexpr
def balexpr(entries, options_map):
    errors = []
    accounts = []

    real_root = realization.RealAccount('')

    balexpr_entries = [entry for entry in entries if is_balexpr_entry(entry)]

    asserted_accounts = {
        account_
        for entry in balexpr_entries
        for account_ in get_accounts_from_entry(entry)
    }

    asserted_match_list = [
        account.parent_matcher(account_) for account_ in asserted_accounts
    ]
    for account_ in getters.get_accounts(entries):
        if (account_ in asserted_accounts
                or any(match(account_) for match in asserted_match_list)):
            realization.get_or_create(real_root, account_)

    open_close_map = getters.get_account_open_close(entries)

    current_checking_balexpr_entry = 0

    for entry in entries:
        if current_checking_balexpr_entry >= len(balexpr_entries):
            break

        while current_checking_balexpr_entry < len(
                balexpr_entries) and balexpr_entries[
                    current_checking_balexpr_entry].date == entry.date:
            checking_entry = balexpr_entries[current_checking_balexpr_entry]
            current_checking_balexpr_entry += 1

            accounts = get_accounts_from_entry(checking_entry)
            if not accounts:
                errors.append(
                    BalExprError(checking_entry.meta,
                                 'No account found in the expression',
                                 checking_entry))
                continue

            currency = get_expected_amount_from_entry(checking_entry).currency
            error_found_in_currencies = False
            for account_ in accounts:
                try:
                    open, _ = open_close_map[account_]
                except KeyError:
                    errors.append(
                        BalExprError(
                            checking_entry.meta,
                            'Invalid reference to unknown account \'{}\''.
                            format(account_), checking_entry))
                    error_found_in_currencies = True
                    break

                if currency not in open.currencies:
                    errors.append(
                        BalExprError(checking_entry.meta,
                                     'Currencies are inconsistent',
                                     checking_entry))
                    error_found_in_currencies = True
                    break

            if error_found_in_currencies:
                continue

            expression = get_expression_from_entry(checking_entry)
            expected_amount = get_expected_amount_from_entry(checking_entry)

            real_amount, error_msg = calcuate(expression, currency, real_root)
            if error_msg:
                errors.append(
                    BalExprError(checking_entry.meta, error_msg,
                                 checking_entry))
                continue

            diff_amount = sub(real_amount, expected_amount)
            if abs(diff_amount.number) > 0.005:
                errors.append(
                    BalExprError(
                        checking_entry.meta,
                        "BalExpr failed: expected {} != accumulated {} ({} {})"
                        .format(expected_amount, real_amount,
                                abs(diff_amount.number),
                                ('too much' if diff_amount.number > 0 else
                                 'too little')), checking_entry))

        if isinstance(entry, Transaction):
            for posting in entry.postings:
                real_account = realization.get(real_root, posting.account)
                if real_account is not None:
                    real_account.balance.add_position(posting)

    return entries, errors