def monthly_income_expenses_totals(self): month_tuples = self._month_tuples(self.entries) monthly_totals = [] for begin_date, end_date in month_tuples: entries, index = summarize.clamp_opt(self.entries, begin_date, end_date + timedelta(days=1), self.options_map) income_totals = self._table_totals(realization.get(realization.realize(entries, self.account_types), self.options_map['name_income'])) expenses_totals = self._table_totals(realization.get(realization.realize(entries, self.account_types), self.options_map['name_expenses'])) # FIXME find better way to only include relevant totals (lots of ZERO-ones at the beginning) sum_ = ZERO for currency, number in income_totals.items(): sum_ += number for currency, number in expenses_totals.items(): sum_ += number if sum_ != ZERO: monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'income_totals': income_totals, 'expenses_totals': expenses_totals }) return monthly_totals
def render_real_htmldiv(self, real_root, price_map, price_date, options_map, file): # Render the income statement tables. operating_currencies = options_map['operating_currency'] income = tree_table.table_of_balances( realization.get(real_root, options_map['name_income']), price_map, price_date, operating_currencies, self.formatter) expenses = tree_table.table_of_balances( realization.get(real_root, options_map['name_expenses']), price_map, price_date, operating_currencies, self.formatter) file.write(""" <div id="income" class="halfleft"> <div id="income"> <h3>Income</h3> {income} </div> </div> <div class="halfright"> <div id="expenses"> <h3>Expenses</h3> {expenses} </div> </div> """.format(**locals()))
def monthly_income_expenses_totals(self): month_tuples = self._month_tuples(self.entries) monthly_totals = [] for begin_date, end_date in month_tuples: entries = self._entries_in_inclusive_range(begin_date, end_date) realized = realization.realize(entries, self.account_types) income_totals = self._table_totals(realization.get(realized, self.account_types.income)) expenses_totals = self._table_totals(realization.get(realized, self.account_types.expenses)) # FIXME find better way to only include relevant totals (lots of ZERO-ones at the beginning) sum_ = ZERO for currency, number in income_totals.items(): sum_ += number for currency, number in expenses_totals.items(): sum_ += number if sum_ != ZERO: monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'income_totals': income_totals, 'expenses_totals': expenses_totals }) return monthly_totals
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 render_real_htmldiv(self, real_root, options_map, file): account_types = options.get_account_types(options_map) for root in (account_types.assets, account_types.liabilities): oss = io.StringIO() table = tree_table.tree_table(oss, realization.get(real_root, root), None, ['Account', 'Last Entry']) num_cells = 0 for real_account, cells, row_classes in table: if not isinstance(real_account, realization.RealAccount): continue last_posting = realization.find_last_active_posting( real_account.txn_postings) # Don't render updates to accounts that have been closed. # Note: this is O(N), maybe we can store this at realization. if last_posting is None or isinstance(last_posting, data.Close): continue last_date = data.get_entry(last_posting).date # Skip this posting if a cutoff date has been specified and the # account has been updated to at least the cutoff date. if self.args.cutoff and self.args.cutoff <= last_date: continue # Okay, we need to render this. Append. cells.append( data.get_entry(last_posting).date if real_account. txn_postings else '-') num_cells += 1 if num_cells: file.write(oss.getvalue())
def render_real_text(self, real_root, price_map, price_date, options_map, file): rows = [] account_types = options.get_account_types(options_map) for root in (account_types.assets, account_types.liabilities): for unused_first_line, unused_cont_line, real_account in realization.dump( realization.get(real_root, root)): last_posting = realization.find_last_active_posting( real_account.txn_postings) # Don't render updates to accounts that have been closed. # Note: this is O(N), maybe we can store this at realization. if last_posting is None or isinstance(last_posting, data.Close): continue last_date = data.get_entry(last_posting).date # Skip this posting if a cutoff date has been specified and the # account has been updated to at least the cutoff date. if self.args.cutoff and self.args.cutoff <= last_date: continue rows.append((real_account.account, last_date)) table_ = table.create_table(rows, [(0, 'Account'), (1, 'Last Date', '{}'.format)]) table.render_table(table_, file, 'text')
def test_is_account_active(self, entries, _, __): """ 2014-01-01 open Assets:Inactive 2014-01-01 open Assets:Active 2014-01-01 open Equity:Other 2014-07-04 * Assets:Active 1 USD Equity:Other """ real_root = realization.realize(entries) self.assertFalse(tree_table.is_account_active( realization.get(real_root, 'Assets:Inactive'))) self.assertTrue(tree_table.is_account_active( realization.get(real_root, 'Assets:Active')))
def monthly_income_expenses_totals(self): month_tuples = self._interval_tuples('month', self.entries) monthly_totals = [] for begin_date, end_date in month_tuples: entries = self._entries_in_inclusive_range(self.entries, begin_date, end_date) realized = realization.realize(entries, self.account_types) income_totals = self._table_totals(realization.get(realized, self.account_types.income)) expenses_totals = self._table_totals(realization.get(realized, self.account_types.expenses)) monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'income_totals': income_totals, 'expenses_totals': expenses_totals }) return monthly_totals
def journal(self, account_name=None): if account_name: real_account = realization.get(self.real_accounts, account_name) else: real_account = self.real_accounts postings = realization.get_postings(real_account) return self._journal_for_postings(postings)
def create_report(entries, options_map): real_root = realization.realize(entries) # Find the institutions from the data. groups, ignored_accounts = find_institutions(entries, options_map) # List all the asset accounts which aren't included in the report. oc_map = getters.get_account_open_close(entries) open_map = {acc: open_entry for acc, (open_entry, _) in oc_map.items()} for acc in sorted(ignored_accounts): logging.info("Ignored account: %s", acc) # Gather missing fields and create a report object. institutions = [] for name, accounts in sorted(groups.items()): # Get the institution fields, which is the union of the fields for all # the accounts with the institution fields. institution_accounts = [ acc for acc in accounts if 'institution' in open_map[acc].meta ] institution_fields = {} for acc in institution_accounts: for key, value in open_map[acc].meta.items(): institution_fields.setdefault(key, value) institution_fields.pop('filename', None) institution_fields.pop('lineno', None) # Create infos for each account in this institution. account_reports = [] for acc in accounts: account_fields = {} for subacc in account.parents(acc): open_entry = open_map[subacc] if 'institution' in open_entry.meta: break account_fields.update(open_entry.meta) account_fields.pop('filename', None) account_fields.pop('lineno', None) for field in institution_fields: account_fields.pop(field, None) real_node = realization.get(real_root, acc) account_reports.append( AccountReport( acc, open_entry.date, real_node.balance, sum(1 for posting in real_node.txn_postings if isinstance(posting, data.TxnPosting)), account_fields)) # Create the institution report. institution = InstitutionReport(name, institution_fields, account_reports) institutions.append(institution) return Report(options_map['title'], institutions)
def test_compute_balance(self): real_root = create_real([('Assets:US:Bank:Checking', '100 USD'), ('Assets:US:Bank:Savings', '200 USD'), ('Assets:US:Bank', '10 USD'), ('Liabilities:Bank:CreditCard', '-500 USD')]) balance = realization.compute_balance(real_root) self.assertEqual(inventory.from_string('-190 USD'), balance) balance = realization.compute_balance(realization.get(real_root, 'Assets:US:Bank')) self.assertEqual(inventory.from_string('310 USD'), balance)
def monthly_income_expenses_totals(self): month_tuples = self._interval_tuples('month', self.entries) monthly_totals = [] for begin_date, end_date in month_tuples: entries = self._entries_in_inclusive_range(self.entries, begin_date, end_date) realized = realization.realize(entries, self.account_types) income_totals = self._table_totals( realization.get(realized, self.account_types.income)) expenses_totals = self._table_totals( realization.get(realized, self.account_types.expenses)) monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'income_totals': income_totals, 'expenses_totals': expenses_totals }) return monthly_totals
def test_should_show(app): with app.test_request_context('/'): app.preprocess_request() assert should_show(g.api.root_account) is True assert should_show(g.api.root_account) is True with app.test_request_context('/?time=2100'): app.preprocess_request() assert not g.api.fava_options['show-accounts-with-zero-balance'] assert should_show(g.api.root_account) is True empty_account = realization.get(g.api.root_account, 'Expenses') assert should_show(empty_account) is False
def render_real_htmldiv(self, real_root, options_map, file): operating_currencies = options_map['operating_currency'] assets = tree_table.table_of_balances( realization.get(real_root, options_map['name_assets']), operating_currencies, self.formatter) liabilities = tree_table.table_of_balances( realization.get(real_root, options_map['name_liabilities']), operating_currencies, self.formatter) equity = tree_table.table_of_balances( realization.get(real_root, options_map['name_equity']), operating_currencies, self.formatter) file.write(""" <div class="halfleft"> <div id="assets"> <h3>Assets</h3> {assets} </div> </div> <div class="halfright"> <div id="liabilities"> <h3>Liabilities</h3> {liabilities} </div> <div class="spacer"> </div> <div id="equity"> <h3>Equity</h3> {equity} </div> </div> """.format(**locals()))
def _real_accounts(self, account_name, entries, begin_date=None, end_date=None): """ Returns the realization.RealAccount instances for account_name, and their entries clamped by the optional begin_date and end_date. Warning: For efficiency, the returned result does not include any added postings to account for balances at 'begin_date'. :return: realization.RealAccount instances """ entries_in_range = self._entries_in_inclusive_range(entries, begin_date=begin_date, end_date=end_date) real_accounts = realization.get(realization.realize(entries_in_range, [account_name]), account_name) return real_accounts
def test_get(self): ra0 = create_simple_account() self.assertEqual('Assets', realization.get(ra0, 'Assets').account) self.assertEqual('Assets:US:Bank', realization.get(ra0, 'Assets:US:Bank').account) self.assertEqual('Assets:US:Bank:Checking', realization.get(ra0, 'Assets:US:Bank:Checking').account) self.assertEqual(None, realization.get(ra0, 'Assets:US:Bank:Savings')) self.assertEqual(42, realization.get(ra0, 'Assets:US:Bank:Savings', 42)) with self.assertRaises(ValueError): self.assertEqual(42, realization.get(ra0, None)) self.assertEqual(None, realization.get(ra0, ''))
def test_simple_realize(self, entries, errors, options_map): """ 2013-05-01 open Assets:US:Checking:Sub USD 2013-05-01 open Expenses:Stuff 2013-05-02 txn "Testing!" Assets:US:Checking:Sub 100 USD Expenses:Stuff -100 USD """ real_root = realization.realize(entries) for real_account in realization.iter_children(real_root): assert isinstance(real_account, realization.RealAccount) for account_name in ['Assets:US:Checking:Sub', 'Expenses:Stuff']: real_account = realization.get(real_root, account_name) self.assertEqual(account_name, real_account.account)
def zip_real_accounts(ra_list): if not ra_list: return first = ra_list[0] return { 'account': first.account, 'balance_and_balance_children': [(serialize_inventory(ra.balance, at_cost=True), serialize_inventory(realization.compute_balance(ra), at_cost=True)) for ra in ra_list], 'children': [zip_real_accounts([realization.get(ra, n) for ra in ra_list]) for n, a in sorted(first.items())], }
def zip_real_accounts(ra_list): if not ra_list: return first = ra_list[0] return { 'account': first.account, 'balance_and_balance_children': [(serialize_inventory(ra.balance, at_cost=True), serialize_inventory(realization.compute_balance(ra), at_cost=True)) for ra in ra_list], 'children': [ zip_real_accounts([realization.get(ra, n) for ra in ra_list]) for n, a in sorted(first.items()) ], }
def _real_account(account_name, entries, begin_date=None, end_date=None, min_accounts=None): """ Returns the realization.RealAccount instances for account_name, and their entries clamped by the optional begin_date and end_date. Warning: For efficiency, the returned result does not include any added postings to account for balances at 'begin_date'. :return: realization.RealAccount instances """ if begin_date: entries = list(iter_entry_dates(entries, begin_date, end_date)) if not min_accounts: min_accounts = [account_name] return realization.get(realization.realize(entries, min_accounts), account_name)
def main(): logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = argparse.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount input file') parser.add_argument('accounts', nargs='+', help='Account names') parser.add_argument('--date', type=date_utils.parse_date_liberally, help="Date") parser.add_argument('-o', '--output', action='store', help="Output directory for the CSV files") args = parser.parse_args() entries, errors, options_map = loader.load_file(args.filename) # Filter out anything after the given date. if args.date is None: args.date = entries[-1].date entries = [entry for entry in entries if entry.date < args.date] # Compute the balance in each account and process it. real_root = realization.realize(entries) rows = [] fieldspec = list(enumerate(['Vest Date', 'Units', 'Instrument', 'Cost'])) for account in args.accounts: real_acc = realization.get(real_root, account) if real_acc is None: logging.error("Account '%s' does not exist", account) continue for position in real_acc.balance: rows.append((position.cost.date, position.units.number, position.units.currency, position.cost.number, position.cost.currency)) rows = sorted(rows) tbl = table.create_table(rows, fieldspec) table.render_table(tbl, sys.stdout, 'text') if args.output: with open(path.join(args.output), 'w') as file: table.render_table(tbl, file, 'csv')
def journal(self, account_name=None, with_change_and_balance=False, with_journal_children=True): if account_name: if not account_name in [account['full_name'] for account in self.all_accounts]: return [] real_account = realization.get(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = [] postings.extend(real_account.txn_postings) postings.sort(key=posting_sortkey) return self._journal_for_postings(postings, with_change_and_balance=with_change_and_balance) else: return self._journal_for_postings(self.entries, with_change_and_balance=with_change_and_balance)
def get_postings(self, real_root): """Return the postings corresponding to the account filter option. Args: real_root: A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. """ if self.args.account: real_account = realization.get(real_root, self.args.account) if real_account is None: raise report.ReportError("Invalid account name: {}".format( self.args.account)) else: real_account = real_root return realization.get_postings(real_account)
def _real_account(self, account_name, entries, begin_date=None, end_date=None, min_accounts=None): """ Returns the realization.RealAccount instances for account_name, and their entries clamped by the optional begin_date and end_date. Warning: For efficiency, the returned result does not include any added postings to account for balances at 'begin_date'. :return: realization.RealAccount instances """ if begin_date: entries = list(iter_entry_dates(entries, begin_date, end_date)) if not min_accounts: min_accounts = [account_name] return realization.get(realization.realize(entries, min_accounts), account_name)
def setUp(self, entries, _, __): """ plugin "beancount.plugins.implicit_prices" 2014-01-01 open Assets:Checking 2014-01-01 open Assets:Investing 2014-01-01 open Assets:Savings 2014-01-01 open Income:MountainOfMoney 2014-01-01 open Equity:Opening-Balances 2014-01-05 pad Assets:Checking Equity:Opening-Balances 2014-02-10 balance Assets:Checking 100.00 USD 2014-03-04 ! "Salary" Income:MountainOfMoney -4000.00 USD Assets:Checking 2014-03-05 balance Assets:Checking 4100.00 USD 2014-03-10 note Assets:Checking "Something to say" 2014-03-11 document Assets:Checking "/path/to/document.pdf" ;; With link. 2014-03-17 * "Transfer" ^784375fd5a68 Assets:Checking -2000 USD Assets:Checking -1500 USD Assets:Investing:Cash 3500 USD 2014-03-25 * "Investment" Assets:Investing:Cash -3000 USD Assets:Investing:Stock 30 STOCK {100 USD} ;; Failing. 2014-05-01 balance Assets:Checking 0.00 USD 2014-12-31 close Assets:Checking """ self.entries = entries real_root = realization.realize(self.entries) self.real_account = realization.get(real_root, 'Assets:Checking') self.postings = realization.get_postings(self.real_account)
def test_realize(self): input_string = """ 2012-01-01 open Expenses:Restaurant 2012-01-01 open Expenses:Movie 2012-01-01 open Assets:Cash 2012-01-01 open Liabilities:CreditCard 2012-01-01 open Equity:Opening-Balances 2012-01-15 pad Liabilities:CreditCard Equity:Opening-Balances 2012-03-01 * "Food" Expenses:Restaurant 100 CAD Assets:Cash 2012-03-10 * "Food again" Expenses:Restaurant 80 CAD Liabilities:CreditCard ;; Two postings on the same account. 2012-03-15 * "Two Movies" Expenses:Movie 10 CAD Expenses:Movie 10 CAD Liabilities:CreditCard 2012-03-20 note Liabilities:CreditCard "Called Amex, asked about 100 CAD dinner" 2012-03-28 document Liabilities:CreditCard "{filename}" 2013-04-01 balance Liabilities:CreditCard 204 CAD 2014-01-01 close Liabilities:CreditCard """ # 'filename' is because we need an existing filename. entries, errors, _ = loader.load_string( input_string.format(filename=__file__)) real_account = realization.realize(entries) ra0_movie = realization.get(real_account, 'Expenses:Movie') self.assertEqual('Expenses:Movie', ra0_movie.account) expected_balance = inventory.Inventory() expected_balance.add_amount(A('20 CAD')) self.assertEqual(expected_balance, ra0_movie.balance)
def _real_accounts(self, account_name, begin_date=None, end_date=None): """ Returns the realization.RealAccount instances for account_name, and their entries clamped by the optional begin_date and end_date. Returns: realization.RealAccount instances """ begin_date_, end_date_ = getters.get_min_max_dates(self.entries, (Transaction)) if begin_date: begin_date_ = begin_date if end_date: end_date_ = end_date entries, index = summarize.clamp_opt(self.entries, begin_date_, end_date_ + timedelta(days=1), self.options_map) real_accounts = realization.get(realization.realize(entries, self.account_types), account_name) return real_accounts
def get_postings(self, real_root): """Return the postings corresponding to the account filter option. Args: real_root: A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. """ if self.args.account: real_account = realization.get(real_root, self.args.account) if real_account is None: # If the account isn't found, return an empty list of postings. # Note that this used to return the following error. # raise base.ReportError( # "Invalid account name: {}".format(self.args.account)) return [] else: real_account = real_root return realization.get_postings(real_account)
def _real_accounts(self, account_name, entries, begin_date=None, end_date=None): """ Returns the realization.RealAccount instances for account_name, and their entries clamped by the optional begin_date and end_date. Warning: For efficiency, the returned result does not include any added postings to account for balances at 'begin_date'. :return: realization.RealAccount instances """ entries_in_range = self._entries_in_inclusive_range( entries, begin_date=begin_date, end_date=end_date) real_accounts = realization.get( realization.realize(entries_in_range, [account_name]), account_name) return real_accounts
def test_find_last_active_posting(self, entries, _, __): """ 2012-01-01 open Assets:Target 2012-01-01 open Equity:Other 2014-02-01 * Assets:Target 123.45 CAD Equity:Other 2014-03-01 U "This should get ignored because of the unrealized flag." Assets:Target -23.45 CAD Equity:Other ;; This should get ignored because it's not one of the directives checked for ;; active. 2014-03-02 event "location" "Somewhere, Somewhereland" """ real_account = realization.realize(entries) txn_postings = realization.get(real_account, 'Assets:Target').txn_postings txn_posting = realization.find_last_active_posting(txn_postings) self.assertEqual(datetime.date(2014, 2, 1), txn_posting.txn.date)
def journal(self, account_name=None, with_change_and_balance=False, with_journal_children=True): if account_name: if not account_name in [ account['full_name'] for account in self.all_accounts ]: return [] real_account = realization.get(self.root_account, account_name) if with_journal_children: postings = realization.get_postings(real_account) else: postings = [] postings.extend(real_account.txn_postings) postings.sort(key=posting_sortkey) return self._journal_for_postings( postings, with_change_and_balance=with_change_and_balance) else: return self._journal_for_postings( self.entries, with_change_and_balance=with_change_and_balance)
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 test_get_or_create(example_api): assert get_or_create(example_api.root_account, '') == \ example_api.root_account assert get_or_create(example_api.root_account, 'Expenses') == \ realization.get(example_api.root_account, 'Expenses')
def monthly_totals(self, account_name): real_account = realization.get(self.real_accounts, account_name) return self._monthly_totals(real_account.account, self.entries)
def read_assets(filename, currency, reduce_accounts, quantization): """Read a Beancount file and produce a list of assets. Args: filename: A string, the path to the Beancount file to read. currency: A string, the currency to convert all the contents to. reduce_accounts: A set of account names to be aggregated. quantization: A Decimal instance, to quantize all the resulting amounts. Returns: A list of (account-name, number-balance), numbers being assumed to be in the requested currency. """ # Read the Beancount input file. entries, _, options_map = loader.load_file(filename, log_errors=logging.error) acctypes = options.get_account_types(options_map) price_map = prices.build_price_map(entries) ocmap = getters.get_account_open_close(entries) # Compute aggregations. real_root = realization.realize(entries, compute_balance=True) # Reduce accounts which have been found in details (mutate the tree in-place). for account in reduce_accounts: real_acc = realization.get(real_root, account) real_acc.balance = realization.compute_balance(real_acc) real_acc.clear() # Prune all the closed accounts and their parents. real_root = prune_closed_accounts(real_root, ocmap) # Produce a list of accounts and their balances reduced to a single currency. acceptable_types = (acctypes.assets, acctypes.liabilities) accounts = [] for real_acc in realization.iter_children(real_root): atype = account_types.get_account_type(real_acc.account) if atype not in acceptable_types: continue try: _, close = ocmap[real_acc.account] if close is not None: continue except KeyError: #logging.info("Account not there: {}".format(real_acc.account)) if real_acc.account not in reduce_accounts and real_acc.balance.is_empty( ): continue value_inv = real_acc.balance.reduce( lambda x: convert.get_value(x, price_map)) currency_inv = value_inv.reduce(convert.convert_position, currency, price_map) amount = currency_inv.get_currency_units(currency) accounts.append( (real_acc.account, amount.number.quantize(quantization))) # Reduce this list of (account-name, balance-number) sorted by reverse amount order. accounts.sort(key=lambda x: x[1], reverse=True) return accounts
def yearly_totals(self, account_name): real_account = realization.get(self.root_account, account_name) return self._interval_totals(real_account.account, 'year', self.entries)
def test_get_or_create(example_ledger: FavaLedger) -> None: assert (get_or_create(example_ledger.all_root_account, "") == example_ledger.all_root_account) assert get_or_create(example_ledger.all_root_account, "Expenses") == realization.get( example_ledger.all_root_account, "Expenses")
def test_get_or_create(example_ledger): assert (get_or_create(example_ledger.root_account, "") == example_ledger.root_account) assert get_or_create(example_ledger.root_account, "Expenses") == realization.get( example_ledger.root_account, "Expenses")
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 get_balance(account, currency, real_root): real_account = realization.get(real_root, account) subtree_balance = realization.compute_balance(real_account, leaf_only=False) return subtree_balance.get_currency_units(currency)