class HoldingsReport(base.TableReport): """The full list of holdings for Asset and Liabilities accounts.""" names = ['holdings'] aggregations = { 'commodity': dict(aggregation_key=lambda holding: holding.currency), 'account': dict(aggregation_key=lambda holding: holding.account), 'root-account': dict( aggregation_key=lambda holding: account.root(3, holding.account), sort_key=lambda holding: holding.market_value or ZERO), 'currency': dict(aggregation_key=lambda holding: holding.cost_currency), } def __init__(self, *rest, **kwds): super().__init__(*rest, **kwds) if self.args.relative and not self.args.currency: self.parser.error("--relative needs to have --currency set") @classmethod def add_args(cls, parser): parser.add_argument('-c', '--currency', action='store', default=None, help="Which currency to convert all the holdings to") parser.add_argument('-r', '--relative', action='store_true', help="True if we should render as relative values only") parser.add_argument('-g', '--groupby', '--by', action='store', default=None, choices=cls.aggregations.keys(), help="How to group the holdings (default is: don't group)") def generate_table(self, entries, errors, options_map): keywords = self.aggregations[self.args.groupby] if self.args.groupby else {} return report_holdings(self.args.currency, self.args.relative, entries, options_map, **keywords) def render_beancount(self, entries, errors, options_map, file): # Don't allow any aggregations if we output as beancount format. for attribute in 'currency', 'relative', 'groupby': if getattr(self.args, attribute): self.parser.error( "'beancount' format does not support --{} option".format(attribute)) # Get the summarized entries and print them out. holdings_entries = get_holdings_entries(entries, options_map) dcontext = options_map['dcontext'] printer.print_entries(holdings_entries, dcontext, file=file)
def test_root(self): name = "Liabilities:US:Credit-Card:Blue" self.assertEqual("", account.root(0, name)) self.assertEqual("Liabilities", account.root(1, name)) self.assertEqual("Liabilities:US", account.root(2, name)) self.assertEqual("Liabilities:US:Credit-Card", account.root(3, name)) self.assertEqual("Liabilities:US:Credit-Card:Blue", account.root(4, name)) self.assertEqual("Liabilities:US:Credit-Card:Blue", account.root(5, name))
def option(self, filename, lineno, key, value): """Process an option directive. Args: filename: current filename. lineno: current line number. key: option's key (str) value: option's value """ if key not in self.options: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, "Invalid option: '{}'".format(key), None)) elif key in options.READ_ONLY_OPTIONS: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, "Option '{}' may not be set".format(key), None)) else: option_descriptor = options.OPTIONS[key] # Issue a warning if the option is deprecated. if option_descriptor.deprecated: meta = new_metadata(filename, lineno) self.errors.append( DeprecatedError(meta, option_descriptor.deprecated, None)) # Convert the value, if necessary. if option_descriptor.converter: try: value = option_descriptor.converter(value) except ValueError as exc: meta = new_metadata(filename, lineno) self.errors.append( ParserError( meta, "Error for option '{}': {}".format(key, exc), None)) return option = self.options[key] if isinstance(option, list): # Append to a list of values. option.append(value) elif isinstance(option, dict): # Set to a dict of values. if not (isinstance(value, tuple) and len(value) == 2): self.errors.append( ParserError( meta, "Error for option '{}': {}".format(key, value), None)) return dict_key, dict_value = value option[dict_key] = dict_value elif isinstance(option, bool): # Convert to a boolean. if not isinstance(value, bool): value = (value.lower() in {'true', 'on'}) or (value == '1') self.options[key] = value else: # Fix up account_rounding to be a subaccount if the user specified a # full account name. This is intended to ease transition in the # change of semantics that occurred on 2015-09-05, whereby the value # of this option became defined as a subaccount of Equity instead of # a full account name. See Issue #67. # This should eventually be deprecated, say, in a year (after Sep 2016). if key == 'account_rounding': root = account.root(1, value) if root in (self.options['name_{}'.format(name)] for name in [ 'assets', 'liabilities', 'equity', 'income', 'expenses' ]): self.errors.append( ParserError( self.get_lexer_location(), "'account_rounding' option should now refer to " "a subaccount.", None)) value = account.sans_root(value) # Set the value. self.options[key] = value # Refresh the list of valid account regexps as we go along. if key.startswith('name_'): # Update the set of valid account types. self.account_regexp = valid_account_regexp(self.options)
def root(acc, n): """Get the root name(s) of the account.""" return account.root(n, acc)
def get_asset(entry): # returns the asset/liability account of a transactino for post in entry.postings: if acc.root(1, post.account) in ['Assets', 'Liabilities']: return post raise Exception(f"entry did not have Asset/ Liability posting: {entry}")
def get_income(entry): # returns the income/expense account of a transactino for post in entry.postings: if acc.root(1, post.account) in ['Income', 'Expenses']: return post raise Exception(f"entry did not have Income/ Expense posting: {entry}")
def __call__(self, context): args = self.eval_args(context) return account.root(args[1], args[0])