def test_contains(self): ra0 = RealAccount('') realization.get_or_create(ra0, 'Assets:US:Bank:Checking') realization.get_or_create(ra0, 'Assets:US:Bank:Savings') self.assertTrue(realization.contains(ra0, 'Assets:US:Bank:Checking')) self.assertTrue(realization.contains(ra0, 'Assets:US:Bank:Savings')) self.assertFalse(realization.contains(ra0, 'Assets:US:Cash'))
def test_compare_realizations(self): # Check that value comparison uses our balance comparison properly. map1 = {'Assets:US:Bank:Checking': inventory.Inventory()} map2 = {'Assets:US:Bank:Checking': inventory.Inventory()} map2['Assets:US:Bank:Checking'].add_amount(A('0.01 USD')) self.assertNotEqual(map1, map2) # Now check this with accounts. root1 = RealAccount('') ra1 = realization.get_or_create(root1, 'Assets:US:Bank:Checking') ra1.balance.add_amount(A('0.01 USD')) root2 = RealAccount('') ra2 = realization.get_or_create(root2, 'Assets:US:Bank:Checking') ra2.balance.add_amount(A('0.01 USD')) self.assertEqual(ra1, ra2) root3 = copy.deepcopy(root2) ra3 = realization.get(root3, 'Assets:US:Bank:Checking') ra3.account = 'Liabilities:US:CreditCard' self.assertNotEqual(root1, root3) root3 = copy.deepcopy(root2) ra3 = realization.get(root3, 'Assets:US:Bank:Checking') ra3.balance.add_amount(A('0.01 CAD')) self.assertNotEqual(root1, root3) root3 = copy.deepcopy(root2) ra3 = realization.get(root3, 'Assets:US:Bank:Checking') ra3.txn_postings.append('posting') self.assertNotEqual(root1, root3) root3 = copy.deepcopy(root2) ra3 = realization.get(root3, 'Assets:US:Bank:Checking') ra3['Sub'] = RealAccount('Assets:US:Bank:Checking:Sub') self.assertNotEqual(root1, root3)
def test_iter_children(self): ra0 = RealAccount('') for account_name in ['Assets:US:Bank:Checking', 'Assets:US:Bank:Savings', 'Assets:US:Cash', 'Assets:CA:Cash']: realization.get_or_create(ra0, account_name) # Test enumerating all accounts. self.assertEqual(['', 'Assets', 'Assets:CA', 'Assets:CA:Cash', 'Assets:US', 'Assets:US:Bank', 'Assets:US:Bank:Checking', 'Assets:US:Bank:Savings', 'Assets:US:Cash'], [ra.account for ra in realization.iter_children(ra0)]) # Test enumerating leaves only. self.assertEqual(['Assets:CA:Cash', 'Assets:US:Bank:Checking', 'Assets:US:Bank:Savings', 'Assets:US:Cash'], [ra.account for ra in realization.iter_children(ra0, True)])
def test_get_or_create(self): ra0 = RealAccount('') ra0_checking = realization.get_or_create(ra0, 'Assets:US:Bank:Checking') realization.get_or_create(ra0, 'Assets:US:Bank:Savings') self.assertEqual('Assets:US:Bank:Checking', ra0_checking.account) self.assertEqual({'Assets'}, ra0.keys()) self.assertEqual({'Checking', 'Savings'}, ra0['Assets']['US']['Bank'].keys()) ra0_assets = ra0['Assets'] ra0_assets2 = realization.get_or_create(ra0, 'Assets') self.assertTrue(ra0_assets2 is ra0_assets)
def get_or_create( account: realization.RealAccount, account_name: str ) -> realization.RealAccount: """Get or create a child account.""" if account.account == account_name: return account return realization.get_or_create(account, account_name)
def account_uptodate_status(self, account_name): """Status of the last balance. Args: account_name: An account name. Returns: A status string for the last balance of the account, as well as the date of the last balance. - 'green': A balance check that passed. - 'red': A balance check that failed. """ real_account = realization.get_or_create(self.ledger.all_root_account, account_name) status = None date = None for txn_posting in reversed(real_account.txn_postings): if isinstance(txn_posting, Balance): date = txn_posting.date if txn_posting.diff_amount: status = "red" break # XXX check date status = "green" break return status, date
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 render_mini_balances(entries, options_map, conversion=None, price_map=None): """Render a treeified list of the balances for the given transactions. Args: entries: A list of selected transactions to render. options_map: The parsed options. conversion: Conversion method string, None, 'value' or 'cost'. price_map: A price map from the original entries. If this isn't provided, the inventories are rendered directly. If it is, their contents are converted to market value. """ # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(entries) dformat = options_map['dcontext'].build(alignment=Align.DOT, reserved=2) # TODO(blais): I always want to be able to convert at cost. We need # arguments capability. # # TODO(blais): Ideally this conversion inserts a new transactions to # 'Unrealized' to account for the difference between cost and market value. # Insert one and update the realization. Add an update() method to the # realization, given a transaction. acctypes = options.get_account_types(options_map) if conversion == 'value': assert price_map is not None # Warning: Mutate the inventories in-place, converting them to market # value. balance_diff = inventory.Inventory() for real_account in realization.iter_children(real_root): balance_cost = real_account.balance.reduce(convert.get_cost) balance_value = real_account.balance.reduce(convert.get_value, price_map) real_account.balance = balance_value balance_diff.add_inventory(balance_cost) balance_diff.add_inventory(-balance_value) if not balance_diff.is_empty(): account_unrealized = account.join(acctypes.income, options_map["account_unrealized_gains"]) unrealized = realization.get_or_create(real_root, account_unrealized) unrealized.balance.add_inventory(balance_diff) elif conversion == 'cost': for real_account in realization.iter_children(real_root): real_account.balance = real_account.balance.reduce(convert.get_cost) realization.dump_balances(real_root, dformat, file=sys.stdout) # Print out net income change. net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income))
def account_uptodate_status(self, account_name): """Status of the last balance or transaction. Args: account_name: An account name. Returns: A status string for the last balance or transaction of the account. - 'green': A balance check that passed. - 'red': A balance check that failed. - 'yellow': Not a balance check. """ real_account = realization.get_or_create(self.all_root_account, account_name) for txn_posting in reversed(real_account.txn_postings): if isinstance(txn_posting, Balance): if txn_posting.diff_amount: return 'red' return 'green' if isinstance(txn_posting, TxnPosting) and \ txn_posting.txn.flag != FLAG_UNREALIZED: return 'yellow' return None
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 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 account_uptodate_status(self, account_name): """Status of the last balance or transaction. Args: account_name: An account name. Returns: A status string for the last balance or transaction of the account. - 'green': A balance check that passed. - 'red': A balance check that failed. - 'yellow': Not a balance check. """ real_account = realization.get_or_create(self.all_root_account, account_name) for txn_posting in reversed(real_account.txn_postings): if isinstance(txn_posting, Balance): if txn_posting.diff_amount: return "red" return "green" if (isinstance(txn_posting, TxnPosting) and txn_posting.txn.flag != FLAG_UNREALIZED): return "yellow" return None
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, 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_open_metadata(self, account_name): real_account = realization.get_or_create(self.root_account, account_name) postings = realization.get_postings(real_account) for posting in postings: if isinstance(posting, Open): return posting.meta return {}
def hierarchy(self, account_name, begin=None, end=None): """An account tree.""" if begin: entries = iter_entry_dates(self.ledger.entries, begin, end) root_account = realization.realize(entries) else: root_account = self.ledger.root_account return _serialize_real_account( realization.get_or_create(root_account, account_name), end)
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 _last_balance_or_transaction(self, account_name): real_account = realization.get_or_create(self.all_root_account, account_name) for txn_posting in reversed(real_account.txn_postings): if not isinstance(txn_posting, (TxnPosting, Balance)): continue if isinstance(txn_posting, TxnPosting) and txn_posting.txn.flag == flags.FLAG_UNREALIZED: continue return txn_posting
def account_metadata(self, account_name): """Metadata of the account. This is read from the Open entry of the account. """ real_account = realization.get_or_create(self.root_account, account_name) for posting in real_account.txn_postings: if isinstance(posting, Open): return posting.meta return {}
def last_entry(self, account_name): """The last entry of the account if it is not a Close entry. """ account = realization.get_or_create(self.all_root_account, account_name) last = realization.find_last_active_posting(account.txn_postings) if last is None or isinstance(last, Close): return return get_entry(last)
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 _last_balance_or_transaction(self, account_name): real_account = realization.get_or_create(self.all_root_account, account_name) for txn_posting in reversed(real_account.txn_postings): if not isinstance(txn_posting, (TxnPosting, Balance)): continue if isinstance(txn_posting, TxnPosting) and \ txn_posting.txn.flag == flags.FLAG_UNREALIZED: continue return txn_posting
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 last_account_activity_in_days(self, account_name): real_account = realization.get_or_create(self.all_root_account, account_name) last_posting = realization.find_last_active_posting( real_account.txn_postings) if last_posting is None or isinstance(last_posting, Close): return 0 entry = get_entry(last_posting) return (datetime.date.today() - entry.date).days
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 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 account_metadata(self, account_name): """Metadata of the account. Args: account_name: An account name. Returns: Metadata of the Open entry of the account. """ real_account = realization.get_or_create(self.all_root_account, account_name) for posting in real_account.txn_postings: if isinstance(posting, Open): return posting.meta return {}
def journal(self, account_name=None, with_change_and_balance=False, with_journal_children=True): if account_name: 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 self._journal(postings, with_change_and_balance=True) else: return self._journal( self.entries, with_change_and_balance=with_change_and_balance)
def last_entry(self, account_name: str) -> Directive | None: """Get last entry of an account. Args: account_name: An account name. Returns: The last entry of the account if it is not a Close entry. """ account = realization.get_or_create(self.all_root_account, account_name) last = realization.find_last_active_posting(account.txn_postings) if last is None or isinstance(last, Close): return None return get_entry(last)
def _last_posting_for_account(self, account_name): """ Returns the last posting for an account (ignores Close) """ real_account = realization.get_or_create(self.all_root_account, account_name) last_posting = realization.find_last_active_posting( real_account.txn_postings) if not isinstance(last_posting, Close): return last_posting postings = realization.get_postings(real_account) if len(postings) >= 2: return postings[-2] return None
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 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
def get_or_create(account, account_name): """Get or create a child account.""" if account.account == account_name: return account return realization.get_or_create(account, account_name)
def _real_account(account_name, entries, begin_date, end_date): if begin_date: entries = list(iter_entry_dates(entries, begin_date, end_date)) return realization.get_or_create(realization.realize(entries), account_name)
def balances(self, account_name): return realization.get_or_create(self.root_account, account_name)
def create_real(account_value_pairs): real_root = RealAccount('') for account_name, value in account_value_pairs: real_account = realization.get_or_create(real_root, account_name) real_account.balance += inventory.from_string(value) return real_root
def closing_balances(self, account_name): closing_entries = summarize.cap_opt(self.entries, self.options) return realization.get_or_create(realization.realize(closing_entries), account_name)