Пример #1
0
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
Пример #2
0
    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)
Пример #3
0
    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)
Пример #4
0
 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)
Пример #5
0
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
Пример #6
0
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
Пример #7
0
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
Пример #8
0
    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
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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
Пример #12
0
 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(""))
Пример #13
0
def parent(acc):
    """Get the parent name of the account."""
    return account.parent(acc)
Пример #14
0
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
Пример #15
0
 def __call__(self, context):
     args = self.eval_args(context)
     return account.parent(args[0])
Пример #16
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