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)
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
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
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()
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()