Example #1
0
    def test_compute_entry_context(self, entries, _, __):
        """
        2014-01-01 open Assets:Account1
        2014-01-01 open Assets:Account2
        2014-01-01 open Assets:Account3
        2014-01-01 open Assets:Account4
        2014-01-01 open Assets:Other

        2014-02-10 *
          Assets:Account1      100.00 USD
          Assets:Other

        2014-02-11 *
          Assets:Account2       80.00 USD
          Assets:Other

        2014-02-12 *
          Assets:Account3       60.00 USD
          Assets:Account3       40.00 USD
          Assets:Other

        2014-02-20 * #context
          Assets:Account1       5.00 USD
          Assets:Account2      -5.00 USD

        2014-02-21 balance  Assets:Account1   105.00 USD

        2014-02-25 *
          Assets:Account3       5.00 USD
          Assets:Account4      -5.00 USD

        """
        for entry in entries:
            if (isinstance(entry, data.Transaction) and entry.tags
                    and 'context' in entry.tags):
                break
        balance_before, balance_after = interpolate.compute_entry_context(
            entries, entry)

        self.assertEqual(inventory.from_string('100.00 USD'),
                         balance_before['Assets:Account1'])
        self.assertEqual(inventory.from_string('80.00 USD'),
                         balance_before['Assets:Account2'])

        self.assertEqual(inventory.from_string('105.00 USD'),
                         balance_after['Assets:Account1'])
        self.assertEqual(inventory.from_string('75.00 USD'),
                         balance_after['Assets:Account2'])

        # Get the context for an entry that is not a Transaction and ensure that
        # the before and after context is the same.
        for entry in entries:
            if isinstance(entry, data.Balance):
                break
        balance_before, balance_after = interpolate.compute_entry_context(
            entries, entry)
        self.assertEqual(balance_before, balance_after)
Example #2
0
    def context(
        self, entry_hash: str
    ) -> tuple[Directive, dict[str, list[str]] | None, dict[str, list[str]]
               | None, str, str, ]:
        """Context for an entry.

        Arguments:
            entry_hash: Hash of entry.

        Returns:
            A tuple ``(entry, before, after, source_slice, sha256sum)`` of the
            (unique) entry with the given ``entry_hash``. If the entry is a
            Balance or Transaction then ``before`` and ``after`` contain
            the balances before and after the entry of the affected accounts.
        """
        entry = self.get_entry(entry_hash)
        source_slice, sha256sum = get_entry_slice(entry)
        if not isinstance(entry, (Balance, Transaction)):
            return entry, None, None, source_slice, sha256sum

        balances = compute_entry_context(self.all_entries, entry)
        before = {
            acc: [pos.to_string() for pos in sorted(inv)]
            for acc, inv in balances[0].items()
        }
        after = {
            acc: [pos.to_string() for pos in sorted(inv)]
            for acc, inv in balances[1].items()
        }
        return entry, before, after, source_slice, sha256sum
Example #3
0
    def context(self, entry_hash: str) -> Tuple[Directive, Any, str, str]:
        """Context for an entry.

        Arguments:
            entry_hash: Hash of entry.

        Returns:
            A tuple ``(entry, balances, source_slice, sha256sum)`` of the
            (unique) entry with the given ``entry_hash``. If the entry is a
            Balance or Transaction then ``balances`` is a 2-tuple containing
            the balances before and after the entry of the affected accounts.
        """
        entry = self.get_entry(entry_hash)
        balances = None
        if isinstance(entry, (Balance, Transaction)):
            balances = compute_entry_context(self.all_entries, entry)
        source_slice, sha256sum = get_entry_slice(entry)
        return entry, balances, source_slice, sha256sum
Example #4
0
    def context(self, entry_hash):
        """Context for an entry.

        Arguments:
            entry_hash: Hash of entry.

        Returns:
            A tuple ``(entry, balances, source_slice, sha256sum)`` of the
            (unique) entry with the given ``entry_hash``. If the entry is a
            Balance or Transaction then ``balances`` is a 2-tuple containing
            the balances before and after the entry of the affected accounts.
        """
        entry = self.get_entry(entry_hash)
        balances = None
        if isinstance(entry, (Balance, Transaction)):
            balances = interpolate.compute_entry_context(
                self.all_entries, entry)
        source_slice, sha256sum = get_entry_slice(entry)
        return entry, balances, source_slice, sha256sum
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()
Example #6
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()