def validate_directory(accounts, document_dir): """Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with ":" replaced with "/"), check that they refer to an account name declared in the given list. Args: account: A set or dict of account names. document_dir: A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. """ # Generate all parent accounts in the account_set we're checking against, so # that parent directories with no corresponding account don't warn. accounts_with_parents = set(accounts) for account_ in accounts: while True: parent = account.parent(account_) if not parent: break if parent in accounts_with_parents: break accounts_with_parents.add(parent) account_ = parent errors = [] for directory, account_name, _, _ in account.walk(document_dir): if account_name not in accounts_with_parents: errors.append( ValidateDirectoryError( "Invalid directory '{}': no corresponding account '{}'". format(directory, account_name))) return errors
def ancestors(self, name): """Ancestors of an account. Args: name: An account name. Yields: The ancestors of the given account from the bottom up. """ while name: name = account.parent(name) yield self.get(name)
def ancestors(self, name: str) -> Generator[TreeNode, None, None]: """Ancestors of an account. Args: name: An account name. Yields: The ancestors of the given account from the bottom up. """ while name: name = account.parent(name) yield self.get(name)
def getter(entry, key): """Lookup the value working up the accounts tree.""" value = entry.meta.get(key, None) if value is not None: return value account_name = account.parent(entry.account) if not account_name: return defaults.get(key, None) parent_entry = accounts_map.get(account_name, None) if not parent_entry: return defaults.get(key, None) return getter(parent_entry, key)
def get_root_accounts(postings): """Compute a mapping of accounts to root account name. This removes sub-accounts where the leaf of the account name is equal to the currency held within, and all other leaf accounts of such root accounts. Args: postings: A list of Posting instances. Returns: A dict of account names to root account names. """ roots = {posting.account: ( account.parent(posting.account) if (account.leaf(posting.account) == posting.units.currency or (account.leaf(posting.account) == "Cash" and posting.account != "Assets:Cash")) else posting.account) for posting in postings} values = set(roots.values()) for root in list(roots): parent = account.parent(root) if parent in values: roots[root] = parent return roots
def find_parent(mapping, account_name): """Find the deepest parent account present in a given dict or set. Note that this begins and includes the given account name itself. Args: mapping: A dict or set instance. account_name: A string, the name of an account. Returns: The first parent account name found. Raises: KeyError: If none of the parents of 'account_name' can be found in 'mapping.' """ while account_name: if account_name in mapping: return account_name else: account_name = account.parent(account_name) raise KeyError
def abbreviate_account(acc: str, accounts_map: Dict[str, data.Open]): """Compute an abbreviated version of the account name.""" # Get the root of the account by inspecting the "root: TRUE" attribute up # the accounts tree. racc = acc while racc: racc = account.parent(racc) dopen = accounts_map.get(racc, None) if dopen and dopen.meta.get('root', False): acc = racc break # Remove the account type. acc = account.sans_root(acc) # Remove the two-letter country code if there is one. if re.match(r'[A-Z][A-Z]', acc): acc = account.sans_root(acc) return acc
def get(self, name, insert=False): """Get an account. Args: name: An account name. insert: If True, insert the name into the tree if it does not exist. Returns: TreeNode: The account of that name or an empty account if the account is not in the tree. """ try: return self[name] except KeyError: node = TreeNode(name) if insert: if name: parent = self.get(account.parent(name), insert=True) parent.children.append(node) self[name] = node return node
def populate_with_parents(accounts_map, default): """For each account key, propagate the values from their parent account. Args: tax_map: A dict of account name string to some value or None. default: The default value to assign to those accounts for which we cannot resolve a non-null value. Returns: An updated dict with None values of child accounts filled from the closest values of their parent accounts. """ new_accounts_map = accounts_map.copy() for acc in accounts_map.keys(): curacc = acc while curacc: value = accounts_map.get(curacc, None) if value is not None: break curacc = account.parent(curacc) new_accounts_map[acc] = value or default return new_accounts_map
def tree(open_close, start=None, end=None): """Creates a tree (using `dict`s) of account/groups. Respects `ordering:` metadata, otherwise asciibetic by account name. Args: open_close: The dictionary returned from `refried.get_account_entries()`. start: (optional) Limits accounts to those that are `refried.isopen()` during this time frame. end: (optional) Limits accounts to those that are `refried.isopen()` during this time frame. Returns: A recursive `dict` of account string -> (Open/Custom directive, subtree of children). """ accts = _accounts_sorted(open_close, start, end) seen = set() tree = dict() for a, (o, _) in accts: subtree = tree for parent in _reverse_parents(acctops.parent(a)): subtree = subtree.setdefault(parent, (None, OrderedDict()))[1] if parent not in seen: seen.add(parent) subtree[a] = (o, OrderedDict()) return tree
def walk(open_close, root, start=None, end=None): """A generator that yields accounts/groups in preorder... order. Respects `ordering:` metadata, otherwise asciibetic by account name. Args: open_close: The dictionary returned from `refried.get_account_entries()`. root: The account string of the root node to start from. start: (optional) Limits accounts to those that are `refried.isopen()` during this time frame. end: (optional) Limits accounts to those that are `refried.isopen()` during this time frame. Yields: Tuples (account string, Open/Custom directive) in preorder traversal. """ accts = _accounts_sorted(open_close, start, end) accts = filter(lambda cat: cat[0].startswith(root), accts) seen = set() for a, (o, _) in accts: for parent in _reverse_parents(acctops.parent(a)): if parent not in seen: seen.add(parent) yield parent, None seen.add(a) yield a, o
def test_parent(self): self.assertEqual("Expenses:Toys", account.parent("Expenses:Toys:Computer")) self.assertEqual("Expenses", account.parent("Expenses:Toys")) self.assertEqual("", account.parent("Expenses")) self.assertEqual(None, account.parent(""))
def parent(acc): """Get the parent name of the account.""" return account.parent(acc)
def categorize_accounts(account: Account, accounts: Set[Account], atypes: tuple) -> Dict[Account, Cat]: """Categorize the type of accounts for a particular stock. Our purpose is to make the types of postings generic, so they can be categorized and handled generically later on. The patterns used in this file depend on the particular choices of account names in my chart of accounts, and for others to use this, this needs to be specialized somewhat. """ accpath = accountlib.join(*accountlib.split(account)[1:-1]) currency = accountlib.leaf(account) accounts = set(accounts) catmap = {} def move(acc, category): if acc in accounts: accounts.remove(acc) catmap[acc] = category # The account itself. move(account, Cat.ASSET) # The adjacent cash account. move(accountlib.join(atypes.assets, accpath, "Cash"), Cat.CASH) # The adjacent P/L and interest account. move(accountlib.join(atypes.income, accpath, "PnL"), Cat.PNL) move(accountlib.join(atypes.income, accountlib.parent(accpath), "PnL"), Cat.PNL) move(accountlib.join(atypes.income, accpath, "Interest"), Cat.INTEREST) # The associated dividend account. move(accountlib.join(atypes.income, accpath, currency, "Dividend"), Cat.DIVIDEND) # Rounding error account. move("Equity:RoundingError", Cat.ROUNDING) move("Expenses:Losses", Cat.ROUNDING) move("Income:US:MSSB:RoundingVariance", Cat.ROUNDING) for acc in list(accounts): # Employer match and corresponding tracking accounts in IRAUSD. if re.match( "({}|{}):.*:Match401k".format(atypes.assets, atypes.expenses), acc): move(acc, Cat.TRACKING) elif re.search(r"\b(Vested|Unvested)", acc): move(acc, Cat.TRACKING) # Dividends for other stocks. elif re.search(r":Dividends?$", acc): move(acc, Cat.OTHERDIVIDEND) # Direct contribution from employer. elif re.match("{}:.*:Match401k$".format(atypes.income), acc): move(acc, Cat.CASH) elif re.search(":GSURefund$", acc): move(acc, Cat.CASH) # Expenses accounts. elif acc.startswith(atypes.expenses): move(acc, Cat.EXPENSES) elif acc.endswith(":Commissions"): # Income..Commissions move(acc, Cat.EXPENSES) # Currency accounts. elif acc.startswith("Equity:CurrencyAccounts"): move(acc, Cat.CONVERSIONS) # Other cash or checking accounts. elif acc.endswith(":Cash"): move(acc, Cat.CASH) elif acc.endswith(":Checking"): move(acc, Cat.CASH) elif acc.endswith("Receivable"): move(acc, Cat.CASH) # Other stock. elif re.match("{}:[A-Z_.]+$".format(accountlib.parent(acc)), acc): move(acc, Cat.OTHERASSET) else: print("ERROR: Unknown account: {}".format(acc)) move(acc, Cat.UNKNOWN) return catmap
def __call__(self, context): args = self.eval_args(context) return account.parent(args[0])
def load_data(self): import yaml products = {} with open(os.path.join(self.repo_path, "static", "products.yml"), "rt") as f: raw_products = yaml.load(f) if type(raw_products) != list: raise TypeError("Products should be a list") for raw_product in raw_products: product = Product(raw_product) if product.currency in products: raise UpdateFailed("Duplicate product %s" % (product.name, )) products[product.currency] = product product_currencies = { product.currency for product in products.values() } # Load ledger ledger_data, errors, options = beancount.loader.load_file( os.path.join(self.repo_path, "bartab.beancount")) if errors: error_stream = io.StringIO("Failed to load ledger\n") beancount.parser.printer.print_errors(errors, error_stream) raise UpdateFailed(error_stream.getvalue()) accounts = {} accounts_raw = {} # TODO: Handle this using a realization balances = { row.account: row.balance for row in beancount.query.query.run_query( ledger_data, options, """ select account, sum(position) as balance where PARENT(account) = "Liabilities:Bar:Members" OR account = "Assets:Cash:Bar" group by account """)[1] } for entry in ledger_data: if not isinstance(entry, bcdata.Open): continue if not bcacct.parent( entry.account ) == "Liabilities:Bar:Members" and entry.account != "Assets:Cash:Bar": print("Didn't load %s as it's no bar account" % (entry.account, )) continue acct = Member(entry.account, item_curencies=product_currencies) if "display_name" in entry.meta: acct.display_name = entry.meta["display_name"] acct.balance = balances.get(acct.account, bcinv.Inventory()) accounts[acct.internal_name] = acct accounts_raw[acct.account] = acct # That's all the data loaded; now we update this class's fields self.accounts_raw = accounts_raw self.accounts = accounts self.products = products self.bc_options_map = options