def render_account(self, account_name): """See base class.""" if self.view_links: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format( len(components) * self.EMS_PER_COMPONENT) anchor = '<a href="{}" class="account">{}</a>'.format( self.build_url( 'journal', account_name=self.account_xform.render(account_name)), account.leaf(account_name)) return '<span "account" style="padding-left: {}em">{}</span>'.format( indent, anchor) else: anchor = '<a href="{}" class="account">{}</a>'.format( self.build_url( 'journal', account_name=self.account_xform.render(account_name)), account_name) return '<span "account">{}</span>'.format(anchor) else: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format( len(components) * self.EMS_PER_COMPONENT) account_name = account.leaf(account_name) return '<span "account" style="padding-left: {}em">{}</span>'.format( indent, account_name) else: return '<span "account">{}</span>'.format(account_name)
def sort_key(self, entry): logger.debug(f"Sorting {entry} on {self.sort_method}") account_sort_key = () amount_sort_key = 0 text_sort_key = "" method = self.sort_method account_sort_key = tuple(account.split(entry.meta['_account'])) # Not sure in what cases this is needed, but just incase if isinstance(entry, data.TxnPosting): entry = entry.txn if isinstance(entry, data.Transaction) and entry.postings: # Sort on the first account for posting in entry.postings: amount_sort_key = -abs( posting.price.number) if posting.price else 0 break text_sort_key = entry.narration if isinstance(entry, data.Note): account_sort_key = tuple(account.split(entry.account)) text_sort_key = entry.comment if entry.meta.get('filename') == self.merge_file: # If we're dealing with a transaction originating from our existing file, respect the position line_number = entry.meta.get('lineno', 0) else: # Let's put it at the end of the day? line_number = 99999 if method == 'date': return ( entry.meta.get('global-sort', 100), entry.date, entry.meta.get('sort', 100), # entry.meta.get('lineno', 0) if , # Not sure how highly to prioritize existing position. data.SORT_ORDER.get(type(entry), 0), account_sort_key, amount_sort_key, text_sort_key, ) elif method == 'account': sort_key = ( entry.meta.get('global-sort', 100), account_sort_key, entry.date, entry.meta.get('sort', 100), # entry.meta.get('lineno', 0) if , # Not sure how highly to prioritize existing position. data.SORT_ORDER.get(type(entry), 0), amount_sort_key, text_sort_key, ) logger.debug(f"{sort_key}") return sort_key else: raise ValueError( f"Unknown sort method {method}. Try date or account")
def test_account_split(self): account_name = account.split("Expenses:Toys:Computer") self.assertEqual(["Expenses", "Toys", "Computer"], account_name) account_name = account.split("Expenses") self.assertEqual(["Expenses"], account_name) account_name = account.split("") self.assertEqual([""], account_name)
def get_account_type(account_name): """Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_name: A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. """ assert isinstance(account_name, str), "Account is not a string: {}".format(account_name) return account.split(account_name)[0]
def get_account_components(entries): """Gather all the account components available in the given directives. Args: entries: A list of directive instances. Returns: A set of strings, the unique account components, including the root account names. """ accounts = get_accounts(entries) components = set() for account_name in accounts: components.update(account.split(account_name)) return sorted(components)
def get_dict_accounts(account_names): """Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Args: account_names: An iterable of account names (strings) Returns: A nested OrderedDict of account leafs """ leveldict = OrderedDict() for account_name in account_names: nested_dict = leveldict for component in account.split(account_name): nested_dict = nested_dict.setdefault(component, OrderedDict()) nested_dict[get_dict_accounts.ACCOUNT_LABEL] = True return leveldict
def guess_account(self, entry: data.Directive) -> str: # Tag each entry with an "Account" Based on attribute, or best guess on Postings if isinstance(entry, data.Transaction) and entry.postings: logger.debug( f"Guessing account for {entry} in {entry.meta['filename']}") # Sort on the first account for posting in entry.postings: account_parts = account.split(posting.account) if account_parts[0] in ("Assets", "Liabilities"): return posting.account else: return entry.postings[0] elif hasattr(entry, 'account'): return entry.account else: return 'other'
def wishfarm(entries, options_map): errors = [] wish_sprouts = {} for entry in entries: if isinstance(entry, Open): if 'wish_slot' in entry.meta: slot = entry.meta['wish_slot'] if 'name' in entry.meta: errors.append( WishfarmError( entry.meta, 'name should be handled by wishfarm plugin', [entry])) continue tag = entry.meta.get('tag') if tag is None: tag = acctops.split(entry.account)[-1] if slot in wish_sprouts: errors.append( WishfarmError(entry.meta, f'Duplicate wish_slot: "{slot}"', [wish_sprouts[slot], entry])) continue wish_sprouts[slot] = (entry, tag) entry.meta['name'] = f'({tag}) ...' for entry in entries: if isinstance(entry, Custom) and entry.type == "wish-list": if 'wish_slot' in entry.meta: slot = entry.meta['wish_slot'] if slot not in wish_sprouts: errors.append( WishfarmError(entry.meta, f'Unavailable wish_slot: "{slot}"', entry)) continue sprout, slot_name = wish_sprouts.pop(slot) wish_name = entry.values[0].value sprout.meta['wish_name'] = wish_name sprout.meta['wish_filename'] = entry.meta['filename'] sprout.meta['wish_lineno'] = entry.meta['lineno'] sprout.meta.update({ k: v for k, v in entry.meta.items() if k not in sprout.meta }) sprout.meta['name'] = f'({slot_name}) {wish_name}' return entries, errors
def aname(open_close, a, prefix=''): """Retrieve a name for an account/group. If the account/group has an associated `name:` metadata then return that, else return the final component of the account string. Args: open_close: The dictionary returned from `refried.get_account_entries()`. a: The account/group string. prefix: A string to use for tree-like nesting/indentation. Returns: The associated name for the account. """ components = acctops.split(a) start = prefix * (len(components) - 1) if a not in open_close: rest = components[-1] else: rest = open_close[a][0].meta.get('name', components[-1]) return start + rest
def get_leveln_parent_accounts(account_names, level, nrepeats=0): """Return a list of all the unique leaf names are level N in an account hierarchy. Args: account_names: A list of account names (strings) level: The level to cross-cut. 0 is for root accounts. nrepeats: A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. """ leveldict = defaultdict(int) for account_name in set(account_names): components = account.split(account_name) if level < len(components): leveldict[components[level]] += 1 levels = {level_ for level_, count in leveldict.items() if count > nrepeats} return sorted(levels)
def get(real_account, account_name, default=None): """Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of. account_name: A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default: The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. """ if not isinstance(account_name, str): raise ValueError components = account.split(account_name) for component in components: real_child = real_account.get(component, default) if real_child is default: return default real_account = real_child return real_account
def get_or_create(real_account, account_name): """Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of, or create under. account_name: A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. """ if not isinstance(account_name, str): raise ValueError components = account.split(account_name) path = [] for component in components: path.append(component) real_child = real_account.get(component, None) if real_child is None: real_child = RealAccount(account.join(*path)) real_account[component] = real_child real_account = real_child return real_account
def safe_add_entry(self, entry): """Check for possible duplicate in the existing self.entries""" if self.year: if entry.date.year != self.year: return # Loaded Transactions could have a match-key, to help de-duplicate match_key = entry.meta.get('match-key', None) if not match_key: # Roll our own match-key for some things match_key = printer.format_entry(entry) if isinstance(entry, data.Transaction) and entry.postings: # Sort on the first account for posting in entry.postings: account_parts = account.split(posting.account) if account_parts[0] in ("Assets", "Liabilities"): entry.meta['_account'] = posting.account break else: # Use last account? entry.meta['_account'] = entry.postings[0] elif hasattr(entry, 'account'): entry.meta['_account'] = entry.account else: entry.meta['_account'] = 'other' found_match = False existing_entry = None remove_list = [] # TODO do a yaml.load(match_key) to support list count = 0 while match_key and not found_match: existing_entry = None if match_key in self.duplicates: existing_entry = self.duplicates.get(match_key) if existing_entry: # Don't do anything since it's duplicate found_match = True else: # Make note of this match-key self.duplicates[match_key] = entry count += 1 # We support multiple match keys in the format 'match-key-1' .. 'match-key-N' match_key = entry.meta.get(f'match-key-{count}', None) if found_match: # We only "preserve" * entries. Others might be overwritten. if not hasattr(existing_entry, 'flag'): # No need to check flags return # Make sure the existing_entry isn't "booked" with a '*' if existing_entry.flag == entry.flag or existing_entry.flag == '*': return else: # We need to replace the existing entry! remove_list.append(existing_entry) for item in remove_list: if item in self.entries: self.entries.remove(item) self.entries.append(entry)
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 _reverse_parents(a): chain = acctops.split(a) for i in range(len(chain)): yield acctops.join(*chain[:i + 1])