def main():
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())
    parser.add_argument('filename', help='Ledger filename')
    args = parser.parse_args()

    with open('/tmp/ledger.rieg', 'wb') as outfile:
        with riegeli.RecordWriter(outfile) as writer:
            entries, errors, options_map = loader.load_file(args.filename)
            for entry in entries:
                if isinstance(entry, data.Transaction):
                    pbent = convert_Transaction(entry)
                elif isinstance(entry, data.Open):
                    pbent = convert_Open(entry)
                elif isinstance(entry, data.Close):
                    pbent = convert_Close(entry)
                else:
                    pbent = None

                if pbent is not None:
                    #print(type(txn))
                    #print(txn)
                    writer.write_message(pbent)

                if 0:
                    print('-' * 100)
                    printer.print_entry(entry)
                    print(txn)
                    print()
Exemple #2
0
def export_v2_data(filename: str, output_filename: str,
                   num_directives: Optional[int]):
    if output_filename.endswith(".pbtxt"):
        output = open(output_filename, 'w')
        writer = None

        def write(message):
            print(message, file=output)
    else:
        output = open(output_filename, 'wb')
        writer = riegeli.RecordWriter(output)
        write = writer.write_message

    #entries, errors, options_map = loader.load_file(filename)
    entries, errors, options_map = parser.parse_file(filename)
    entries = data.sorted(entries)

    if num_directives:
        entries = itertools.islice(entries, num_directives)
    for entry in entries:
        if isinstance(entry, data.Transaction):
            pbdir = convert_Transaction(entry)
        elif isinstance(entry, data.Open):
            pbdir = convert_Open(entry)
        elif isinstance(entry, data.Close):
            pbdir = convert_Close(entry)
        elif isinstance(entry, data.Commodity):
            pbdir = convert_Commodity(entry)
        elif isinstance(entry, data.Event):
            pbdir = convert_Event(entry)
        elif isinstance(entry, data.Note):
            pbdir = convert_Note(entry)
        elif isinstance(entry, data.Query):
            pbdir = convert_Query(entry)
        elif isinstance(entry, data.Price):
            pbdir = convert_Price(entry)
        elif isinstance(entry, data.Balance):
            pbdir = convert_Balance(entry)
        elif isinstance(entry, data.Pad):
            pbdir = convert_Pad(entry)
        else:
            pbdir = None

        if pbdir is not None:
            write("#---")
            write("# {}".format(pbdir.location.lineno))
            write("#")
            write(pbdir)
            write("")

        if 0:
            print('-' * 80)
            printer.print_entry(entry)
            print(txn)
            print()

    if hasattr(writer, "close"):
        writer.close()
    output.close()
Exemple #3
0
def assert_same_results(actuals: List[Directive], expecteds: List[Directive]):
    same, missings1, missings2 = compare_entries(actuals, expecteds)
    for missing in missings1:
        print('Unexpected entry:', file=sys.stderr)
        printer.print_entry(missing, file=sys.stderr)
    for missing in missings2:
        print('Missing entry:', file=sys.stderr)
        printer.print_entry(missing, file=sys.stderr)
    assert same
Exemple #4
0
def print_unmatched(txn_postings, filename, regexp):
    if not txn_postings:
        return
    print()
    print()
    print('=== Ummatched from {}, "{}"'.format(filename, regexp))
    print()
    for txn_posting in sorted(txn_postings, key=lambda tp: tp.txn.date):
        printer.print_entry(txn_posting.txn)
Exemple #5
0
def main():
    entries, errors, options = loader.load_file(sys.argv[1])

    for entry in entries:
        printer.print_entry(entry)

    print(options, file=sys.stderr)

    for error in errors:
        printer.print_error(error, file=sys.stderr)
Exemple #6
0
 def test_parse_string(self):
   # TODO(blais): Remove, this is temporary, for testing locally.
   filename = os.getenv("L")
   assert filename
   builder = grammar.Builder()
   out = extmodule.parse(builder, filename)
   print(out)
   entries, errors, options_map = builder.finalize()
   #pprint.pprint(options_map)
   pprint.pprint((len(entries), errors))
   if entries:
     printer.print_entry(entries[-1])
Exemple #7
0
def add_ira_contribs(entries, options_map, config):
    """Add legs for 401k employer match contributions.

    See module docstring for an example configuration.

    Args:
      entries: a list of entry instances
      options_map: a dict of options parsed from the file
      config: A configuration string, which is intended to be a Python dict
        mapping match-accounts to a pair of (negative-account, position-account)
        account names.
    Returns:
      A tuple of entries and errors.
    """
    # Parse and extract configuration values.
    config_obj = eval(config, {}, {})
    if not isinstance(config_obj, dict):
        raise RuntimeError(
            "Invalid plugin configuration: should be a single dict.")

    currency = config_obj.pop('currency', 'UNKNOWN')
    flag = config_obj.pop('flag', None)
    account_transforms = config_obj.pop('accounts', {})

    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction):
            orig_entry = entry
            for posting in entry.postings:
                if (posting.account in account_transforms and posting.position
                        and (account_types.get_account_sign(posting.account) *
                             posting.position.number) > 0):

                    # Get the new account legs to insert.
                    neg_account, pos_account = account_transforms[
                        posting.account]
                    assert posting.position.lot.cost is None

                    # Insert income/expense entries for 401k.
                    entry = add_postings(
                        entry,
                        amount.Amount(abs(posting.position.number), currency),
                        neg_account.format(year=entry.date.year),
                        pos_account.format(year=entry.date.year), flag)

            if DEBUG and orig_entry is not entry:
                printer.print_entry(orig_entry)
                printer.print_entry(entry)

        new_entries.append(entry)

    return new_entries, []
Exemple #8
0
    def test_render_missing(self):
        # We want to make sure we never render with scientific notation.
        input_string = textwrap.dedent("""

          2019-01-19 * "Fitness First" "Last training session"
            Expenses:Sports:Gym:Martin
            Assets:Martin:Cash

        """)
        entries, errors, options_map = loader.load_string(input_string)
        txn = errors[0].entry
        oss = io.StringIO()
        printer.print_entry(txn, file=oss)
Exemple #9
0
def main():
    argparser = argparse.ArgumentParser(description=__doc__)
    argparser.add_argument('infile',
                           type=argparse.FileType('r'),
                           help='Filename or "-" for stdin')
    args = argparser.parse_args()

    # Read input from stdin or a given filename.
    entries, errors, options = loader.load_string(args.infile.read())

    # Print out sorted entries.
    for entry in data.sorted(entries):
        printer.print_entry(entry)
Exemple #10
0
def main():
    import argparse, logging
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())
    parser.add_argument('filename', help='Ledger filename')
    args = parser.parse_args()

    entries, errors, options_map = loader.load_file(args.filename)
    for entry in entries:
        if (isinstance(entry, data.Transaction)
                and any(posting.position.lot.lot_date
                        for posting in entry.postings)):
            printer.print_entry(entry)
Exemple #11
0
def main():
    logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())
    parser.add_argument('filename', help='Ledger filename')
    args = parser.parse_args()

    entries, errors, options_map = loader.load_file(args.filename)
    for entry in entries:
        if isinstance(entry, data.Transaction):
            txn = convert_transaction(entry)
            if 0:
                print('-' * 100)
                printer.print_entry(entry)
                print(txn)
                print()
def render_entry_context(entries, options_map, entry):
    """Render the context before and after a particular transaction is applied.

    Args:
      entries: A list of directives.
      options_map: A dict of options, as produced by the parser.
      entry: The entry instance which should be rendered. (Note that this object is
        expected to be in the set of entries, not just structurally equal.)
    Returns:
      A multiline string of text, which consists of the context before the
      transaction is applied, the transaction itself, and the context after it
      is applied. You can just print that, it is in form that is intended to be
      consumed by the user.
    """
    oss = io.StringIO()

    meta = entry.meta
    print("Hash:{}".format(compare.hash_entry(entry)), file=oss)
    print("Location: {}:{}".format(meta["filename"], meta["lineno"]), file=oss)

    # Get the list of accounts sorted by the order in which they appear in the
    # closest entry.
    order = {}
    if isinstance(entry, data.Transaction):
        order = {
            posting.account: index
            for index, posting in enumerate(entry.postings)
        }
    accounts = sorted(getters.get_entry_accounts(entry),
                      key=lambda account: order.get(account, 10000))

    # Accumulate the balances of these accounts up to the entry.
    balance_before, balance_after = interpolate.compute_entry_context(
        entries, entry)

    # Create a format line for printing the contents of account balances.
    max_account_width = max(map(len, accounts)) if accounts else 1
    position_line = '{{:1}} {{:{width}}}  {{:>49}}'.format(
        width=max_account_width)

    # Print the context before.
    print(file=oss)
    print("------------ Balances before transaction", file=oss)
    print(file=oss)
    before_hashes = set()
    for account in accounts:
        positions = balance_before[account].get_positions()
        for position in positions:
            before_hashes.add((account, hash(position)))
            print(position_line.format('', account, str(position)), file=oss)
        if not positions:
            print(position_line.format('', account, ''), file=oss)
        print(file=oss)

    # Print the entry itself.
    print(file=oss)
    print("------------ Transaction", file=oss)
    print(file=oss)
    dcontext = options_map['dcontext']
    printer.print_entry(entry, dcontext, render_weights=True, file=oss)

    if isinstance(entry, data.Transaction):
        print(file=oss)

        # Print residuals.
        residual = interpolate.compute_residual(entry.postings)
        if not residual.is_empty():
            # Note: We render the residual at maximum precision, for debugging.
            print('Residual: {}'.format(residual), file=oss)

        # Dump the tolerances used.
        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
        if tolerances:
            print('Tolerances: {}'.format(', '.join(
                '{}={}'.format(key, value)
                for key, value in sorted(tolerances.items()))),
                  file=oss)

        # Compute the total cost basis.
        cost_basis = inventory.Inventory(pos for pos in entry.postings
                                         if pos.cost is not None).reduce(
                                             convert.get_cost)
        if not cost_basis.is_empty():
            print('Basis: {}'.format(cost_basis), file=oss)

    # Print the context after.
    print(file=oss)
    print("------------ Balances after transaction", file=oss)
    print(file=oss)
    for account in accounts:
        positions = balance_after[account].get_positions()
        for position in positions:
            changed = (account, hash(position)) not in before_hashes
            print(position_line.format('*' if changed else '', account,
                                       str(position)),
                  file=oss)
        if not positions:
            print(position_line.format('', account, ''), file=oss)
        print(file=oss)

    return oss.getvalue()
Exemple #13
0
def segment_periods(entries,
                    accounts_value,
                    accounts_intflows,
                    date_begin=None,
                    date_end=None):
    """Segment entries in terms of piecewise periods of internal flow.

    This function iterated through the given entries and computes balances at
    the beginning and end of periods without external flow entries. You should be
    able to then compute the returns from these informations.

    Args:
      entries: A list of directives. The list may contain directives other than
        than transactions as well as directives with no relation to the assets or
        internal flow accounts (the function simply ignores that which is not
        relevant).
      accounts_value: A set of the asset accounts in the related group.
      accounts_intflows: A set of the internal flow accounts in the related group.
      date_begin: A datetime.date instance, the beginning date of the period to compute
        returns over.
      date_end: A datetime.date instance, the end date of the period to compute returns
        over.
    Returns:
      A pair of
        periods: A list of period tuples, each of which contains:
          period_begin: A datetime.date instance, the first day of the period.
          period_end: A datetime.date instance, the last day of the period.
          balance_begin: An Inventory instance, the balance at the beginning of the period.
          balance_end: An Inventory instance, the balance at the end of the period.
        portfolio_entries: A list of the entries that we used in computing the portfolio.
    Raises:
      ValueError: If the dates create an impossible situation, the beginning
        must come before the requested end, if specified.
    """
    logging.info("Segmenting periods.")
    logging.info("Date begin: %s", date_begin)
    logging.info("Date end:   %s", date_end)

    if date_begin and date_end and date_begin >= date_end:
        raise ValueError("Dates are not ordered correctly: {} >= {}".format(
            date_begin, date_end))

    accounts_related = accounts_value | accounts_intflows
    is_external_flow_entry = lambda entry: (isinstance(
        entry, data.Transaction) and any(posting.account not in
                                         accounts_related
                                         for posting in entry.postings))

    # Create an iterator over the entries we care about.
    portfolio_entries = [
        entry for entry in entries
        if getters.get_entry_accounts(entry) & accounts_value
    ]
    iter_entries = iter(portfolio_entries)
    entry = next(iter_entries)

    # If a beginning cut-off has been specified, skip the entries before then
    # (and make sure to accumulate the initial balance correctly).
    balance = inventory.Inventory()
    if date_begin is not None:
        period_begin = date_begin
        try:
            while True:
                if entry.date >= date_begin:
                    break
                if date_end and entry.date >= date_end:
                    break
                balance = sum_balances_for_accounts(balance, entry,
                                                    accounts_value)
                entry = next(iter_entries)
        except StopIteration:
            # No periods found! Just return an empty list.
            return [(date_begin, date_end or date_begin, balance, balance)], []
    else:
        period_begin = entry.date

    # Main loop over the entries.
    periods = []
    entry_logger = misc_utils.LineFileProxy(logging.debug, '   ')
    done = False
    while True:
        balance_begin = copy.copy(balance)

        logging.debug(
            ",-----------------------------------------------------------")
        logging.debug(" Begin:   %s", period_begin)
        logging.debug(" Balance: %s", balance_begin.units())
        logging.debug("")

        # Consume all internal flow entries, simply accumulating the total balance.
        while True:
            period_end = entry.date
            if is_external_flow_entry(entry):
                break
            if date_end and entry.date >= date_end:
                period_end = date_end
                done = True
                break
            if entry:
                printer.print_entry(entry, file=entry_logger)
            balance = sum_balances_for_accounts(balance, entry, accounts_value)
            try:
                entry = next(iter_entries)
            except StopIteration:
                done = True
                if date_end:
                    period_end = date_end
                break
        else:
            done = True

        balance_end = copy.copy(balance)

        ## FIXME: Bring this back in, this fails for now. Something about the
        ## initialization fails it. assert period_begin <= period_end,
        ## (period_begin, period_end)
        periods.append((period_begin, period_end, balance_begin, balance_end))

        logging.debug(" Balance: %s", balance_end.units())
        logging.debug(" End:     %s", period_end)
        logging.debug(
            "`-----------------------------------------------------------")
        logging.debug("")

        if done:
            break

        # Absorb the balance of the external flow entry.
        assert is_external_flow_entry(entry), entry
        if entry:
            printer.print_entry(entry, file=entry_logger)
        balance = sum_balances_for_accounts(balance, entry, accounts_value)
        try:
            entry = next(iter_entries)
        except StopIteration:
            # If there is an end date, insert that final period to cover the end
            # date, with no changes.
            if date_end:
                periods.append((period_end, date_end, balance, balance))
            break

        period_begin = period_end

    ## FIXME: Bring this back in, this fails for now.
    # assert all(period_begin <= period_end
    #            for period_begin, period_end, _, _ in periods), periods
    return periods, portfolio_entries
Exemple #14
0
def add_ira_contribs(entries, options_map, config_str):
    """Add legs for 401k employer match contributions.

    See module docstring for an example configuration.

    Args:
      entries: a list of entry instances
      options_map: a dict of options parsed from the file
      config_str: A configuration string, which is intended to be a Python dict
        mapping match-accounts to a pair of (negative-account, position-account)
        account names.
    Returns:
      A tuple of entries and errors.
    """
    # Parse and extract configuration values.
    # FIXME: Use ast.literal_eval() here; you need to convert this code and the getters.
    # FIXME: Also, don't raise a RuntimeError, return an error object; review
    # this for all the plugins.
    # FIXME: This too is temporary.
    # pylint: disable=eval-used
    config_obj = eval(config_str, {}, {})
    if not isinstance(config_obj, dict):
        raise RuntimeError(
            "Invalid plugin configuration: should be a single dict.")

    # Currency of the inserted postings.
    currency = config_obj.pop('currency', 'UNKNOWN')

    # Flag to attach to the inserted postings.
    insert_flag = config_obj.pop('flag', None)

    # A dict of account names that trigger the insertion of postings to pairs of
    # inserted accounts when triggered.
    accounts = config_obj.pop('accounts', {})

    # Convert the key in the accounts configuration for matching.
    account_transforms = {}
    for key, config in accounts.items():
        if isinstance(key, str):
            flag = None
            account = key
        else:
            assert isinstance(key, tuple)
            flag, account = key
        account_transforms[account] = (flag, config)

    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction):
            orig_entry = entry
            for posting in entry.postings:
                if (posting.units is not MISSING
                        and (posting.account in account_transforms)
                        and (account_types.get_account_sign(posting.account) *
                             posting.units.number > 0)):

                    # Get the new account legs to insert.
                    required_flag, (
                        neg_account,
                        pos_account) = account_transforms[posting.account]
                    assert posting.cost is None

                    # Check required flag if present.
                    if (required_flag is None or
                        (required_flag and required_flag == posting.flag)):
                        # Insert income/expense entries for 401k.
                        entry = add_postings(
                            entry,
                            amount.Amount(abs(posting.units.number), currency),
                            neg_account.format(year=entry.date.year),
                            pos_account.format(year=entry.date.year),
                            insert_flag)

            if DEBUG and orig_entry is not entry:
                printer.print_entry(orig_entry)
                printer.print_entry(entry)

        new_entries.append(entry)

    return new_entries, []
#!/usr/bin/env python3
from datetime import date
import sys
from beancount import loader
from beancount.core import compare,data
from beancount.parser import printer
from youqianDict import CategoryAll

entries_existing=[]
if len(sys.argv) > 1:
    filename = sys.argv[1]
    entries_existing, errors, options = loader.load_file(filename)

entries_new=[]
accounts_existing=[i.account for i in entries_existing]
for account_youqian in sorted(set(CategoryAll.values())):
    if account_youqian not in accounts_existing:
        entries_new.append(
            data.Open(
                meta=None,
                booking=None,
                date=date(1970, 1, 1),
                account=account_youqian,
                currencies=["CNY"]
            )
        )

for entry in entries_new:
    printer.print_entry(entry)
Exemple #16
0
 def addTransaction(self, transaction, fileName):
     with open(path.join(self.basedir, fileName), 'a') as output:
         printer.print_entry(transaction, file=output)
Exemple #17
0
def get_trades(entries, options_map, symbols, date_end):
    """Process a series of entries and extract a list of processable trades.

    The returned rows include computed fees and proceeds, ready to be washed.
    The list of trades includes buy and sell types.

    Args:
      entries: A list of directives to be processed.
      options_map: An options dict, as per the parser.
      symbols: A set of currency strings for substantially identical stocks.
      date_end: The cutoff date after which to stop processing transactions.
    Returns:

      XXX

    """
    acc_types = options.get_account_types(options_map)

    # Inventory of lots to accumulate.
    balances = inventory.Inventory()

    # A list of trade information.
    trades = []

    for entry in entries:
        # Skip other directives.
        if not isinstance(entry, data.Transaction):
            continue

        # Skip entries after the relevant period.
        if entry.date > date_end:
            continue

        # Skip entries not relevant to the currency.
        if not any(posting.position.lot.currency in symbols
                   for posting in entry.postings):
            continue

        # Calculate the fee amount and the total price, in order to split the
        # fee later on.
        fee_total = ZERO
        units_total = ZERO
        for posting in entry.postings:
            if account_types.get_account_type(
                    posting.account) == acc_types.expenses:
                fee_total += posting.position.number
            if (account_types.get_account_type(
                    posting.account) == acc_types.assets
                    and posting.position.lot.cost is not None):
                units_total += posting.position.number

        # Loop through the postings and create trade entries for them, computing
        # proceeds and fees and all details required to wash sales later on.
        booked = False
        for posting in entry.postings:
            pos = posting.position
            if pos.lot.currency not in symbols:
                continue

            # Check that all sales have the sale price attached to them.
            if pos.number < ZERO:
                assert posting.price or re.search('Split', entry.narration,
                                                  re.I)

            # Update the shared inventory.
            booked = True
            booked_pos = book_position(balances, entry.date, pos)

            # Determine the dates.
            if pos.number > ZERO:
                txn_type = 'BUY'
                acq_date = entry.date
                adj_acq_date = None
                sell_date = None
                number = pos.number
                cost = pos.lot.cost

                price = ''

                partial_fee = ''
                proceeds = ''
                pnl = ''

            else:
                # This only holds true because we book lot sales individually.
                assert len(booked_pos) <= 1, "Internal error."
                booked_position = booked_pos[0]

                txn_type = 'SELL'
                assert pos.number < ZERO
                acq_date = booked_position.lot.lot_date
                adj_acq_date = None
                sell_date = entry.date
                number = pos.number
                cost = pos.lot.cost
                price = posting.price if posting.price else cost

                partial_fee = fee_total * (number / units_total).quantize(
                    D('0.01'))
                proceeds = -number * price.number - partial_fee
                pnl = proceeds - cost_basis

            cost_basis = -number * cost.number

            trades.append(
                (txn_type, acq_date, adj_acq_date, sell_date, number,
                 pos.lot.currency, cost.number, cost_basis,
                 price.number if price else '', proceeds, partial_fee, pnl))

        if booked:
            printer.print_entry(entry)
            for pos in balances:
                number, rest = str(pos).split(' ', 1)
                print('    {:>16} {}'.format(number, rest))
            print()
            print()

    return trades
Exemple #18
0
def render_entry_context(entries, options_map, entry, parsed_entry=None):
    """Render the context before and after a particular transaction is applied.

    Args:
      entries: A list of directives.
      options_map: A dict of options, as produced by the parser.
      entry: The entry instance which should be rendered. (Note that this object is
        expected to be in the set of entries, not just structurally equal.)
      parsed_entry: An optional incomplete, parsed but not booked nor interpolated
        entry. If this is provided, this is used for inspecting the list of prior
        accounts and it is also rendered.
    Returns:
      A multiline string of text, which consists of the context before the
      transaction is applied, the transaction itself, and the context after it
      is applied. You can just print that, it is in form that is intended to be
      consumed by the user.
    """
    oss = io.StringIO()
    pr = functools.partial(print, file=oss)
    header = "** {} --------------------------------"

    meta = entry.meta
    pr(header.format("Transaction Id"))
    pr()
    pr("Hash:{}".format(compare.hash_entry(entry)))
    pr("Location: {}:{}".format(meta["filename"], meta["lineno"]))
    pr()
    pr()

    # Get the list of accounts sorted by the order in which they appear in the
    # closest entry.
    order = {}
    if parsed_entry is None:
        parsed_entry = entry
    if isinstance(parsed_entry, data.Transaction):
        order = {
            posting.account: index
            for index, posting in enumerate(parsed_entry.postings)
        }
    accounts = sorted(getters.get_entry_accounts(parsed_entry),
                      key=lambda account: order.get(account, 10000))

    # Accumulate the balances of these accounts up to the entry.
    balance_before, balance_after = interpolate.compute_entry_context(
        entries, entry, additional_accounts=accounts)

    # Create a format line for printing the contents of account balances.
    max_account_width = max(map(len, accounts)) if accounts else 1
    position_line = '{{:1}} {{:{width}}}  {{:>49}}'.format(
        width=max_account_width)

    # Print the context before.
    pr(header.format("Balances before transaction"))
    pr()
    before_hashes = set()
    average_costs = {}
    for account in accounts:
        balance = balance_before[account]

        pc_balances = balance.split()
        for currency, pc_balance in pc_balances.items():
            if len(pc_balance) > 1:
                average_costs[account] = pc_balance.average()

        positions = balance.get_positions()
        for position in positions:
            before_hashes.add((account, hash(position)))
            pr(position_line.format('', account, str(position)))
        if not positions:
            pr(position_line.format('', account, ''))
        pr()
    pr()

    # Print average cost per account, if relevant.
    if average_costs:
        pr(header.format("Average Costs"))
        pr()
        for account, average_cost in sorted(average_costs.items()):
            for position in average_cost:
                pr(position_line.format('', account, str(position)))
        pr()
        pr()

    # Print the entry itself.
    dcontext = options_map['dcontext']
    pr(header.format("Unbooked Transaction"))
    pr()
    if parsed_entry:
        printer.print_entry(parsed_entry,
                            dcontext,
                            render_weights=True,
                            file=oss)
    pr()

    pr(header.format("Transaction"))
    pr()
    printer.print_entry(entry, dcontext, render_weights=True, file=oss)
    pr()

    if isinstance(entry, data.Transaction):
        pr(header.format("Residual and Tolerances"))
        pr()

        # Print residuals.
        residual = interpolate.compute_residual(entry.postings)
        if not residual.is_empty():
            # Note: We render the residual at maximum precision, for debugging.
            pr('Residual: {}'.format(residual))

        # Dump the tolerances used.
        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
        if tolerances:
            pr('Tolerances: {}'.format(', '.join(
                '{}={}'.format(key, value)
                for key, value in sorted(tolerances.items()))))

        # Compute the total cost basis.
        cost_basis = inventory.Inventory(pos for pos in entry.postings
                                         if pos.cost is not None).reduce(
                                             convert.get_cost)
        if not cost_basis.is_empty():
            pr('Basis: {}'.format(cost_basis))
        pr()
        pr()

    # Print the context after.
    pr(header.format("Balances after transaction"))
    pr()
    for account in accounts:
        positions = balance_after[account].get_positions()
        for position in positions:
            changed = (account, hash(position)) not in before_hashes
            print(position_line.format('*' if changed else '', account,
                                       str(position)),
                  file=oss)
        if not positions:
            pr(position_line.format('', account, ''))
        pr()

    return oss.getvalue()