def account_journal(self, account_name, with_journal_children=False): """Journal for an account. Args: account_name: An account name. with_journal_children: Whether to include postings of subaccounts of the given account. Returns: A list of tuples ``(entry, postings, change, balance)``. change and balance have already been reduced to units. """ real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: # pylint: disable=unused-variable postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [(entry, postings_, copy.copy(change), copy.copy(balance)) for (entry, postings_, change, balance) in realization.iterate_with_balance(postings)]
def linechart(self, account_name): """The balance of an account. Args: account_name: A string. Returns: A list of dicts for all dates on which the balance of the given account has changed containing the balance (in units) of the account at that date. """ real_account = realization.get_or_create(self.ledger.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) # When the balance for a commodity just went to zero, it will be # missing from the 'balance' so keep track of currencies that last had # a balance. last_currencies = None for entry, _, change, balance in journal: if change.is_empty(): continue balance = inv_to_dict(cost_or_value(balance, entry.date)) currencies = set(balance.keys()) if last_currencies: for currency in last_currencies - currencies: balance[currency] = 0 last_currencies = currencies yield {"date": entry.date, "balance": balance}
def account_journal( self, filtered: FilteredLedger, account_name: str, with_journal_children: bool = False, ) -> list[tuple[Directive, list[Posting], Inventory, Inventory]]: """Journal for an account. Args: filtered: The currently filtered ledger. account_name: An account name. with_journal_children: Whether to include postings of subaccounts of the given account. Returns: A list of tuples ``(entry, postings, change, balance)``. change and balance have already been reduced to units. """ real_account = realization.get_or_create(filtered.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [(entry, postings_, copy.copy(change), copy.copy(balance)) for ( entry, postings_, change, balance, ) in realization.iterate_with_balance(postings)]
def account_journal(self, account_name, with_journal_children=False): """Journal for an account. Args: account_name: An account name. with_journal_children: Whether to include postings of subaccounts of the given account. Returns: A list of tuples ``(entry, postings, change, balance)``. change and balance have already been reduced to units. """ real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [(entry, postings_, copy.copy(change), copy.copy(balance)) for ( entry, postings_, change, balance, ) in realization.iterate_with_balance(postings)]
def linechart(self, account_name): """The balance of an account. Args: account_name: A string. Returns: A list of dicts for all dates on which the balance of the given account has changed containing the balance (in units) of the account at that date. """ real_account = realization.get_or_create(self.ledger.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) # When the balance for a commodity just went to zero, it will be # missing from the 'balance' field but appear in the 'change' field. # Use 0 for those commodities. return [{ 'date': entry.date, 'balance': dict({curr: 0 for curr in list(change.currencies())}, **_inventory_units(balance)), } for entry, _, change, balance in journal if len(change)]
def account_journal(self, account_name, with_journal_children=False): real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return realization.iterate_with_balance(postings)
def _journal(self, entries, include_types=None, with_change_and_balance=False): if include_types: entries = [entry for entry in entries if isinstance(entry, include_types)] if not with_change_and_balance: return [serialize_entry(entry) for entry in entries] else: return [serialize_entry_with(entry, change, balance) for entry, _, change, balance in realization.iterate_with_balance(entries)]
def account_journal(self, account_name, with_journal_children=False): real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [serialize_entry_with(entry, change, balance) for entry, _, change, balance in realization.iterate_with_balance(postings)]
def account_journal(self, account_name, with_journal_children=False): real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [ serialize_entry_with(entry, change, balance) for entry, _, change, balance in realization.iterate_with_balance(postings) ]
def linechart(self, account_name): real_account = realization.get_or_create(self.ledger.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) return [{ 'date': entry.date, # when there's no holding for a commodity, it will be missing from # 'balance' field but appear in 'change' field. Use 0 for those # commodities. 'balance': dict({curr: 0 for curr in list(change.currencies())}, **_serialize_inventory(balance)), } for entry, _, change, balance in journal if len(change)]
def linechart(self, account_name): real_account = realization.get_or_create(self.api.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) return [{ 'date': entry.date, # when there's no holding for a commodity, it will be missing from # 'balance' field but appear in 'change' field. Use 0 for those # commodities. 'balance': dict({curr: 0 for curr in list(change.currencies())}, **_serialize_inventory(balance)), } for entry, _, change, balance in journal if len(change)]
def size_and_render_amounts(postings, at_cost, render_balance): """Iterate through postings and compute sizers and render amounts. Args: postings: A list of Posting or directive instances. at_cost: A boolean, if true, render the cost value, not the actual. render_balance: A boolean, if true, renders a running balance column. """ # Compute the maximum width required to render the change and balance # columns. In order to carry this out, we will pre-compute all the data to # render this and save it for later. change_sizer = AmountColumnSizer('change') balance_sizer = AmountColumnSizer('balance') entry_data = [] for entry_line in realization.iterate_with_balance(postings): entry, leg_postings, change, balance = entry_line # Convert to cost if necessary. (Note that this agglutinates currencies, # so we'd rather do make the conversion at this level (inventory) than # convert the positions or amounts later.) if at_cost: change = change.reduce(convert.get_cost) if render_balance: balance = balance.reduce(convert.get_cost) # Compute the amounts and maximum widths for the change column. change_amounts = [] for position in change.get_positions(): units = position.units change_amounts.append(units) change_sizer.update(units.number, units.currency) # Compute the amounts and maximum widths for the balance column. balance_amounts = [] if render_balance: for position in balance.get_positions(): units = position.units balance_amounts.append(units) balance_sizer.update(units.number, units.currency) entry_data.append( (entry, leg_postings, change_amounts, balance_amounts)) return (entry_data, change_sizer, balance_sizer)
def linechart(self, account_name): """The balance of an account. Args: account_name: A string. Returns: A list of dicts for all dates on which the balance of the given account has changed containing the balance (in units) of the account at that date. """ real_account = realization.get_or_create(self.ledger.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) # When the balance for a commodity just went to zero, it will be # missing from the 'balance' field but appear in the 'change' field. # Use 0 for those commodities. for entry, _, change, balance in journal: if change.is_empty(): continue if g.conversion == 'units': bal = {curr: 0 for curr in list(change.currencies())} bal.update({ p.units.currency: p.units.number for p in balance.reduce(convert.get_units) }) else: bal = { p.units.currency: p.units.number for p in cost_or_value(balance, entry.date) } yield { 'date': entry.date, 'balance': bal, }
def linechart(self, filtered: FilteredLedger, account_name: str, conversion: str) -> Generator[DateAndBalance, None, None]: """The balance of an account. Args: account_name: A string. conversion: The conversion to use. Returns: A list of dicts for all dates on which the balance of the given account has changed containing the balance (in units) of the account at that date. """ real_account = realization.get_or_create(filtered.root_account, account_name) postings = realization.get_postings(real_account) journal = realization.iterate_with_balance(postings) # When the balance for a commodity just went to zero, it will be # missing from the 'balance' so keep track of currencies that last had # a balance. last_currencies = None price_map = self.ledger.price_map for entry, _, change, balance_inventory in journal: if change.is_empty(): continue balance = inv_to_dict( cost_or_value(balance_inventory, conversion, price_map, entry.date)) currencies = set(balance.keys()) if last_currencies: for currency in last_currencies - currencies: balance[currency] = 0 last_currencies = currencies yield DateAndBalance(entry.date, balance)
def account_journal(self, account_name, with_journal_children=False): """Journal for an account. Args: account_name: An account name. with_journal_children: Whether to include postings of subaccounts of the given account. Returns: A list of tuples ``(entry, postings, change, balance)``. """ real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: # pylint: disable=unused-variable postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [(entry, postings, change, copy.copy(balance)) for (entry, postings, change, balance) in realization.iterate_with_balance(postings)]
def _journal_for_postings(self, postings, include_types=None): journal = [] for posting, leg_postings, change, entry_balance in realization.iterate_with_balance(postings): if include_types and not isinstance(posting, include_types): continue if isinstance(posting, Transaction) or \ isinstance(posting, Note) or \ isinstance(posting, Balance) or \ isinstance(posting, Open) or \ isinstance(posting, Close) or \ isinstance(posting, Pad) or \ isinstance(posting, Document): # TEMP # if isinstance(posting, TxnPosting): # posting = posting.txn entry = { 'meta': { 'type': posting.__class__.__name__.lower(), 'filename': posting.meta['filename'], 'lineno': posting.meta['lineno'] }, 'date': posting.date, 'hash': compare.hash_entry(posting) } if isinstance(posting, Open): entry['account'] = posting.account entry['currencies'] = posting.currencies entry['booking'] = posting.booking # TODO im html-template if isinstance(posting, Close): entry['account'] = posting.account if isinstance(posting, Note): entry['comment'] = posting.comment if isinstance(posting, Document): entry['account'] = posting.account entry['filename'] = posting.filename if isinstance(posting, Pad): entry['account'] = posting.account entry['source_account'] = posting.source_account if isinstance(posting, Balance): # TODO failed balances entry['account'] = posting.account entry['change'] = { posting.amount.currency: posting.amount.number } entry['balance'] = { posting.amount.currency: posting.amount.number } entry['tolerance'] = posting.tolerance # TODO currency? TODO in HTML-template entry['diff_amount'] = posting.diff_amount # TODO currency? TODO in HTML-template if isinstance(posting, Transaction): if posting.flag == 'P': entry['meta']['type'] = 'padding' entry['flag'] = posting.flag entry['payee'] = posting.payee entry['narration'] = posting.narration entry['tags'] = posting.tags entry['links'] = posting.links entry['change'] = self._inventory_to_json(change) entry['balance'] = self._inventory_to_json(entry_balance) entry['legs'] = [] for posting_ in posting.postings: leg = { 'account': posting_.account, 'flag': posting_.flag, 'hash': entry['hash'] } if posting_.position: leg['position'] = posting_.position.number leg['position_currency'] = posting_.position.lot.currency if posting_.price: leg['price'] = posting_.price.number leg['price_currency'] = posting_.price.currency entry['legs'].append(leg) journal.append(entry) return journal
def _journal_for_postings(self, postings, include_types=None, with_change_and_balance=False): journal = [] for posting, leg_postings, change, entry_balance in realization.iterate_with_balance( postings): if include_types and not isinstance(posting, include_types): continue if isinstance(posting, Transaction) or \ isinstance(posting, Note) or \ isinstance(posting, Balance) or \ isinstance(posting, Open) or \ isinstance(posting, Close) or \ isinstance(posting, Pad) or \ isinstance(posting, Event) or \ isinstance(posting, Document): entry = { 'meta': { 'type': posting.__class__.__name__.lower(), 'filename': posting.meta['filename'], 'lineno': posting.meta['lineno'] }, 'date': posting.date, 'hash': compare.hash_entry(posting), 'metadata': posting.meta.copy() } entry['metadata'].pop("__tolerances__", None) entry['metadata'].pop("filename", None) entry['metadata'].pop("lineno", None) if isinstance(posting, Open): entry['account'] = posting.account entry['currencies'] = posting.currencies if isinstance(posting, Close): entry['account'] = posting.account if isinstance(posting, Event): entry['type'] = posting.type entry['description'] = posting.description if isinstance(posting, Note): entry['comment'] = posting.comment if isinstance(posting, Document): entry['account'] = posting.account entry['filename'] = posting.filename if isinstance(posting, Pad): entry['account'] = posting.account entry['source_account'] = posting.source_account if isinstance(posting, Balance): entry['account'] = posting.account entry['change'] = { posting.amount.currency: posting.amount.number } entry['amount'] = { posting.amount.currency: posting.amount.number } if posting.diff_amount: balance = entry_balance.get_units( posting.amount.currency) entry['diff_amount'] = { posting.diff_amount.currency: posting.diff_amount.number } entry['balance'] = {balance.currency: balance.number} if isinstance(posting, Transaction): if posting.flag == 'P': entry['meta'][ 'type'] = 'padding' # TODO handle Padding, Summarize and Transfer entry['flag'] = posting.flag entry['payee'] = posting.payee entry['narration'] = posting.narration entry['tags'] = posting.tags or [] entry['links'] = posting.links or [] entry['legs'] = [] for posting_ in posting.postings: leg = { 'account': posting_.account, 'flag': posting_.flag, 'hash': entry['hash'] } if posting_.position: leg['position'] = posting_.position.number leg['position_currency'] = posting_.position.lot.currency cost = interpolate.get_posting_weight(posting_) leg['cost'] = cost.number leg['cost_currency'] = cost.currency if posting_.price: leg['price'] = posting_.price.number leg['price_currency'] = posting_.price.currency entry['legs'].append(leg) if with_change_and_balance: if isinstance(posting, Balance): entry['change'] = { posting.amount.currency: posting.amount.number } entry['balance'] = self._inventory_to_json( entry_balance ) #, include_currencies=entry['change'].keys()) if isinstance(posting, Transaction): entry['change'] = self._inventory_to_json(change) entry['balance'] = self._inventory_to_json( entry_balance, include_currencies=entry['change'].keys()) journal.append(entry) return journal
def _journal_for_postings(self, postings, include_types=None, with_change_and_balance=False): journal = [] for posting, leg_postings, change, entry_balance in realization.iterate_with_balance(postings): if include_types and not isinstance(posting, include_types): continue if isinstance(posting, Transaction) or \ isinstance(posting, Note) or \ isinstance(posting, Balance) or \ isinstance(posting, Open) or \ isinstance(posting, Close) or \ isinstance(posting, Pad) or \ isinstance(posting, Event) or \ isinstance(posting, Document): entry = { 'meta': { 'type': posting.__class__.__name__.lower(), 'filename': posting.meta['filename'], 'lineno': posting.meta['lineno'] }, 'date': posting.date, 'hash': compare.hash_entry(posting), 'metadata': posting.meta.copy() } entry['metadata'].pop("__tolerances__", None) entry['metadata'].pop("filename", None) entry['metadata'].pop("lineno", None) if isinstance(posting, Open): entry['account'] = posting.account entry['currencies'] = posting.currencies if isinstance(posting, Close): entry['account'] = posting.account if isinstance(posting, Event): entry['type'] = posting.type entry['description'] = posting.description if isinstance(posting, Note): entry['comment'] = posting.comment if isinstance(posting, Document): entry['account'] = posting.account entry['filename'] = posting.filename if isinstance(posting, Pad): entry['account'] = posting.account entry['source_account'] = posting.source_account if isinstance(posting, Balance): entry['account'] = posting.account entry['change'] = { posting.amount.currency: posting.amount.number } entry['amount'] = { posting.amount.currency: posting.amount.number } if posting.diff_amount: balance = entry_balance.get_units(posting.amount.currency) entry['diff_amount'] = { posting.diff_amount.currency: posting.diff_amount.number } entry['balance'] = { balance.currency: balance.number } if isinstance(posting, Transaction): if posting.flag == 'P': entry['meta']['type'] = 'padding' # TODO handle Padding, Summarize and Transfer entry['flag'] = posting.flag entry['payee'] = posting.payee entry['narration'] = posting.narration entry['tags'] = posting.tags or [] entry['links'] = posting.links or [] entry['legs'] = [] for posting_ in posting.postings: leg = { 'account': posting_.account, 'flag': posting_.flag, 'hash': entry['hash'] } if posting_.position: leg['position'] = posting_.position.number leg['position_currency'] = posting_.position.lot.currency cost = interpolate.get_posting_weight(posting_) leg['cost'] = cost.number leg['cost_currency'] = cost.currency if posting_.price: leg['price'] = posting_.price.number leg['price_currency'] = posting_.price.currency entry['legs'].append(leg) if with_change_and_balance: if isinstance(posting, Balance): entry['change'] = { posting.amount.currency: posting.amount.number } entry['balance'] = self._inventory_to_json(entry_balance) #, include_currencies=entry['change'].keys()) if isinstance(posting, Transaction): entry['change'] = self._inventory_to_json(change) entry['balance'] = self._inventory_to_json(entry_balance, include_currencies=entry['change'].keys()) journal.append(entry) return journal
def test_iterate_with_balance(self, entries, _, __): """ 2012-01-01 open Assets:Bank:Checking 2012-01-01 open Expenses:Restaurant 2012-01-01 open Equity:Opening-Balances 2012-01-15 pad Assets:Bank:Checking Equity:Opening-Balances 2012-01-20 balance Assets:Bank:Checking 20.00 USD 2012-03-01 * "With a single entry" Expenses:Restaurant 11.11 CAD Assets:Bank:Checking 2012-03-02 * "With two entries" Expenses:Restaurant 20.01 CAD Expenses:Restaurant 20.02 CAD Assets:Bank:Checking 2012-03-02 note Expenses:Restaurant "This was good" 2012-04-01 balance Expenses:Restaurant 51.14 CAD """ root_account = realization.realize(entries) real_account = realization.get(root_account, 'Expenses:Restaurant') def simplify_rtuple(rtuple): return [(type(entry), len(postings), str(change), str(balance)) for entry, postings, change, balance in rtuple] # Surprinsingly enough, this covers all the legal cases that occur in # practice (checked for full coverage manually if you like). # pylint: disable=bad-whitespace rtuple = realization.iterate_with_balance( real_account.txn_postings[:-2]) self.assertEqual([ (data.Open, 0, '()', '()'), (data.Transaction, 1, '(11.11 CAD)', '(11.11 CAD)'), (data.Transaction, 2, '(40.03 CAD)', '(51.14 CAD)'), ], simplify_rtuple(rtuple)) # Test it again with the final balance entry. rtuple = realization.iterate_with_balance(real_account.txn_postings) self.assertEqual([ (data.Open, 0, '()', '()'), (data.Transaction, 1, '(11.11 CAD)', '(11.11 CAD)'), (data.Transaction, 2, '(40.03 CAD)', '(51.14 CAD)'), (data.Note, 0, '()', '(51.14 CAD)'), (data.Balance, 0, '()', '(51.14 CAD)'), ], simplify_rtuple(rtuple)) # Test it out with valid input but with entries for the same transaction # separated by another entry. Swap the balance entry with the last # posting entry to test this. postings = list(real_account.txn_postings) postings[-3], postings[-2] = postings[-2], postings[-3] rtuple = realization.iterate_with_balance(postings) self.assertEqual([ (data.Open, 0, '()', '()'), (data.Transaction, 1, '(11.11 CAD)', '(11.11 CAD)'), (data.Transaction, 2, '(40.03 CAD)', '(51.14 CAD)'), (data.Note, 0, '()', '(51.14 CAD)'), (data.Balance, 0, '()', '(51.14 CAD)'), ], simplify_rtuple(rtuple)) # Go one step further and test it out with invalid date ordering. postings = list(real_account.txn_postings) postings[-1], postings[-2] = postings[-2], postings[-1] with self.assertRaises(AssertionError): list(realization.iterate_with_balance(postings))
def iterate_html_postings(txn_postings, formatter): """Iterate through the list of transactions with rendered HTML strings for each cell. This pre-renders all the data for each row to HTML. This is reused by the entries table rendering routines. Args: txn_postings: A list of TxnPosting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. Yields: Instances of Row tuples. See above. """ for entry_line in realization.iterate_with_balance(txn_postings): entry, leg_postings, change, entry_balance = entry_line # Prepare the data to be rendered for this row. balance_str = formatter.render_inventory(entry_balance) rowtype = entry.__class__.__name__ flag = '' extra_class = '' links = None if isinstance(entry, data.Transaction): rowtype = FLAG_ROWTYPES.get(entry.flag, 'Transaction') extra_class = 'warning' if entry.flag == flags.FLAG_WARNING else '' flag = entry.flag description = '<span class="narration">{}</span>'.format(entry.narration) if entry.payee: description = ('<span class="payee">{}</span>' '<span class="pnsep">|</span>' '{}').format(entry.payee, description) amount_str = formatter.render_inventory(change) if entry.links and formatter: links = [formatter.render_link(link) for link in entry.links] elif isinstance(entry, data.Balance): # Check the balance here and possibly change the rowtype if entry.diff_amount is None: description = 'Balance {} has {}'.format( formatter.render_account(entry.account), entry.amount) else: description = ('Balance in {} fails; ' 'expected = {}, balance = {}, difference = {}').format( formatter.render_account(entry.account), entry.amount, entry_balance.get_currency_units(entry.amount.currency), entry.diff_amount) extra_class = 'fail' amount_str = formatter.render_amount(entry.amount) elif isinstance(entry, (data.Open, data.Close)): description = '{} {}'.format(entry.__class__.__name__, formatter.render_account(entry.account)) amount_str = '' elif isinstance(entry, data.Note): description = '{} {}'.format(entry.__class__.__name__, entry.comment) amount_str = '' balance_str = '' elif isinstance(entry, data.Document): assert path.isabs(entry.filename) description = 'Document for {}: {}'.format( formatter.render_account(entry.account), formatter.render_doc(entry.filename)) amount_str = '' balance_str = '' else: description = entry.__class__.__name__ amount_str = '' balance_str = '' yield Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)