def __init__(self, path): #: The path to the main Beancount file. self.beancount_file_path = path self._is_encrypted = is_encrypted_file(path) self._filters = { 'account': AccountFilter(), 'from': FromFilter(), 'payee': PayeeFilter(), 'tag': TagFilter(), 'time': TimeFilter(), } #: An :class:`AttributesModule` instance. self.attributes = AttributesModule(self) #: A :class:`.BudgetModule` instance. self.budgets = BudgetModule(self) #: A :class:`.ChartModule` instance. self.charts = ChartModule(self) #: A :class:`.ExtensionModule` instance. self.extensions = ExtensionModule(self) #: A :class:`.FileModule` instance. self.file = FileModule(self) #: A :class:`.IngestModule` instance. self.ingest = IngestModule(self) #: A :class:`.FavaMisc` instance. self.misc = FavaMisc(self) #: A :class:`.QueryShell` instance. self.query_shell = QueryShell(self) self._watcher = Watcher() #: List of all (unfiltered) entries. self.all_entries = None #: A list of all errors reported by Beancount. self.errors = None #: A Beancount options map. self.options = None #: A Namedtuple containing the names of the five base accounts. self.account_types = None #: A dict with all of Fava's option values. self.fava_options = None self.load_file()
def __init__(self, path: str) -> None: #: The path to the main Beancount file. self.beancount_file_path = path self._is_encrypted = is_encrypted_file(path) #: An :class:`AttributesModule` instance. self.attributes = AttributesModule(self) #: A :class:`.BudgetModule` instance. self.budgets = BudgetModule(self) #: A :class:`.ChartModule` instance. self.charts = ChartModule(self) #: A :class:`.ExtensionModule` instance. self.extensions = ExtensionModule(self) #: A :class:`.FileModule` instance. self.file = FileModule(self) #: A :class:`.IngestModule` instance. self.ingest = IngestModule(self) #: A :class:`.FavaMisc` instance. self.misc = FavaMisc(self) #: A :class:`.DecimalFormatModule` instance. self.format_decimal = DecimalFormatModule(self) #: A :class:`.QueryShell` instance. self.query_shell = QueryShell(self) self._watcher = Watcher() #: List of all (unfiltered) entries. self.all_entries = [] #: Dict of list of all (unfiltered) entries by type. self.all_entries_by_type = group_entries_by_type([]) #: A list of all errors reported by Beancount. self.errors: list[BeancountError] = [] #: A Beancount options map. self.options: BeancountOptions = OPTIONS_DEFAULTS #: A dict containing information about the accounts. self.accounts = AccountDict() #: A dict with commodity names (from the 'name' metadata) self.commodity_names: dict[str, str] = {} #: A dict with all of Fava's option values. self.fava_options: FavaOptions = FavaOptions() self._date_first: datetime.date | None = None self._date_last: datetime.date | None = None self.load_file()
def __init__(self, path): #: The path to the main Beancount file. self.beancount_file_path = path self._is_encrypted = is_encrypted_file(path) self._filters = {} #: An :class:`AttributesModule` instance. self.attributes = AttributesModule(self) #: A :class:`.BudgetModule` instance. self.budgets = BudgetModule(self) #: A :class:`.ChartModule` instance. self.charts = ChartModule(self) #: A :class:`.ExtensionModule` instance. self.extensions = ExtensionModule(self) #: A :class:`.FileModule` instance. self.file = FileModule(self) #: A :class:`.IngestModule` instance. self.ingest = IngestModule(self) #: A :class:`.FavaMisc` instance. self.misc = FavaMisc(self) #: A :class:`.DecimalFormatModule` instance. self.format_decimal = DecimalFormatModule(self) #: A :class:`.QueryShell` instance. self.query_shell = QueryShell(self) self._watcher = Watcher() #: List of all (unfiltered) entries. self.all_entries = None #: Dict of list of all (unfiltered) entries by type. self.all_entries_by_type = None #: A list of all errors reported by Beancount. self.errors = None #: A Beancount options map. self.options = None #: A Namedtuple containing the names of the five base accounts. self.account_types = None #: A dict containing information about the accounts. self.accounts = _AccountDict() #: A dict with all of Fava's option values. self.fava_options = None self.load_file()
def __init__(self, path: str) -> None: #: The path to the main Beancount file. self.beancount_file_path = path self._is_encrypted = is_encrypted_file(path) #: An :class:`AttributesModule` instance. self.attributes = AttributesModule(self) #: A :class:`.BudgetModule` instance. self.budgets = BudgetModule(self) #: A :class:`.ChartModule` instance. self.charts = ChartModule(self) #: A :class:`.ExtensionModule` instance. self.extensions = ExtensionModule(self) #: A :class:`.FileModule` instance. self.file = FileModule(self) #: A :class:`.IngestModule` instance. self.ingest = IngestModule(self) #: A :class:`.FavaMisc` instance. self.misc = FavaMisc(self) #: A :class:`.DecimalFormatModule` instance. self.format_decimal = DecimalFormatModule(self) #: A :class:`.QueryShell` instance. self.query_shell = QueryShell(self) self._watcher = Watcher() #: List of all (unfiltered) entries. self.all_entries = [] #: Dict of list of all (unfiltered) entries by type. self.all_entries_by_type: Dict[Type[Directive], Entries] = {} #: A list of all errors reported by Beancount. self.errors: List[BeancountError] = [] #: A Beancount options map. self.options: Dict[str, Any] = {} #: A dict containing information about the accounts. self.accounts = AccountDict() #: A dict containing information about the commodities self.commodities: Dict[str, Commodity] = {} #: A dict with all of Fava's option values. self.fava_options: FavaOptions = {} self.load_file()
class FavaLedger(): """Create an interface for a Beancount ledger. Arguments: path: Path to the main Beancount file. """ __slots__ = [ '_default_format_string', '_format_string', 'account_types', 'all_entries', 'all_root_account', 'beancount_file_path', '_date_first', '_date_last', 'entries', 'errors', 'fava_options', '_filters', '_is_encrypted', 'options', 'price_map', 'root_account', '_watcher' ] + MODULES def __init__(self, path): #: The path to the main Beancount file. self.beancount_file_path = path self._is_encrypted = is_encrypted_file(path) self._filters = { 'account': AccountFilter(), 'from': FromFilter(), 'payee': PayeeFilter(), 'tag': TagFilter(), 'time': TimeFilter(), } #: An :class:`AttributesModule` instance. self.attributes = AttributesModule(self) #: A :class:`.BudgetModule` instance. self.budgets = BudgetModule(self) #: A :class:`.ChartModule` instance. self.charts = ChartModule(self) #: A :class:`.ExtensionModule` instance. self.extensions = ExtensionModule(self) #: A :class:`.FileModule` instance. self.file = FileModule(self) #: A :class:`.FavaMisc` instance. self.misc = FavaMisc(self) #: A :class:`.QueryShell` instance. self.query_shell = QueryShell(self) self._watcher = Watcher() #: List of all (unfiltered) entries. self.all_entries = None #: A list of all errors reported by Beancount. self.errors = None #: A Beancount options map. self.options = None #: A Namedtuple containing the names of the five base accounts. self.account_types = None #: A dict with all of Fava's option values. self.fava_options = None self.load_file() def load_file(self): """Load the main file and all included files and set attributes.""" # use the internal function to disable cache if not self._is_encrypted: # pylint: disable=protected-access self.all_entries, self.errors, self.options = \ loader._load([(self.beancount_file_path, True)], None, None, None) include_path = os.path.dirname(self.beancount_file_path) self._watcher.update(self.options['include'], [ os.path.join(include_path, path) for path in self.options['documents'] ]) else: self.all_entries, self.errors, self.options = \ loader.load_file(self.beancount_file_path) self.price_map = prices.build_price_map(self.all_entries) self.account_types = get_account_types(self.options) self.all_root_account = realization.realize(self.all_entries, self.account_types) if self.options['render_commas']: self._format_string = '{:,f}' self._default_format_string = '{:,.2f}' else: self._format_string = '{:f}' self._default_format_string = '{:.2f}' self.fava_options, errors = parse_options( filter_type(self.all_entries, Custom)) self.errors.extend(errors) for mod in MODULES: getattr(self, mod).load_file() self.filter(True) # pylint: disable=attribute-defined-outside-init def filter(self, force=False, **kwargs): """Set and apply (if necessary) filters.""" changed = False for filter_name, value in kwargs.items(): if self._filters[filter_name].set(value): changed = True if not (changed or force): return self.entries = self.all_entries for filter_class in self._filters.values(): self.entries = filter_class.apply(self.entries, self.options) self.root_account = realization.realize(self.entries, self.account_types) self._date_first, self._date_last = \ getters.get_min_max_dates(self.entries, (Transaction)) if self._date_last: self._date_last = self._date_last + datetime.timedelta(1) if self._filters['time']: self._date_first = self._filters['time'].begin_date self._date_last = self._filters['time'].end_date def changed(self): """Check if the file needs to be reloaded. Returns: True if a change in one of the included files or a change in a document folder was detected and the file has been reloaded. """ # We can't reload an encrypted file, so act like it never changes. if self._is_encrypted: return False changed = self._watcher.check() if changed: self.load_file() return changed def quantize(self, value, currency): """Quantize the value to the right number of decimal digits. Uses the DisplayContext generated by beancount.""" if not currency: return self._default_format_string.format(value) return self._format_string.format(self.options['dcontext'].quantize( value, currency)) def interval_ends(self, interval): """Generator yielding dates corresponding to interval boundaries.""" if not self._date_first: return [] return date.interval_ends(self._date_first, self._date_last, interval) def get_account_sign(self, account_name): """Get account sign.""" return get_account_sign(account_name, self.account_types) @property def root_account_closed(self): """A root account where closing entries have been generated.""" closing_entries = summarize.cap_opt(self.entries, self.options) return realization.realize(closing_entries) def interval_balances(self, interval, account_name, accumulate=False): """Balances by interval. Arguments: interval: An interval. account_name: An account name. accumulate: A boolean, ``True`` if the balances for an interval should include all entries up to the end of the interval. Returns: A list of RealAccount instances for all the intervals. """ min_accounts = [ account for account in self.attributes.list_accounts(self.all_root_account) if account.startswith(account_name) ] interval_tuples = list( reversed(list(pairwise(self.interval_ends(interval))))) interval_balances = [ realization.realize( list( iter_entry_dates( self.entries, datetime.date.min if accumulate else begin_date, end_date)), min_accounts) for begin_date, end_date in interval_tuples ] return interval_balances, interval_tuples def account_journal(self, account_name, with_journal_children=False): """Journal for an account. Args: account_name: An account name. with_journal_children: Whether to include postings of subaccounts of the given account. Returns: A list of tuples ``(entry, postings, change, balance)``. """ real_account = realization.get_or_create(self.root_account, account_name) if with_journal_children: # pylint: disable=unused-variable postings = realization.get_postings(real_account) else: postings = real_account.txn_postings return [(entry, postings, change, copy.copy(balance)) for (entry, postings, change, balance) in realization.iterate_with_balance(postings)] def events(self, event_type=None): """List events (possibly filtered by type).""" events = list(filter_type(self.entries, Event)) if event_type: return filter(lambda e: e.type == event_type, events) return events def get_entry(self, entry_hash): """Find an entry. Arguments: entry_hash: Hash of the entry. Returns: The entry with the given hash. Raises: FavaAPIException: If there is no entry for the given hash. """ try: return next(entry for entry in self.all_entries if entry_hash == hash_entry(entry)) except StopIteration: raise FavaAPIException( 'No entry found for hash "{}"'.format(entry_hash)) def context(self, entry_hash): """Context for an entry. Arguments: entry_hash: Hash of entry. Returns: A tuple ``(entry, context)`` of the (unique) entry with the given ``entry_hash`` and its context. """ entry = self.get_entry(entry_hash) ctx = render_entry_context(self.all_entries, self.options, entry) return entry, ctx.split("\n", 2)[2] def commodity_pairs(self): """List pairs of commodities. Returns: A list of pairs of commodities. Pairs of operating currencies will be given in both directions not just in the one found in file. """ fw_pairs = self.price_map.forward_pairs bw_pairs = [] for currency_a, currency_b in fw_pairs: if (currency_a in self.options['operating_currency'] and currency_b in self.options['operating_currency']): bw_pairs.append((currency_b, currency_a)) return sorted(fw_pairs + bw_pairs) def prices(self, base, quote): """List all prices.""" all_prices = prices.get_all_prices(self.price_map, "{}/{}".format(base, quote)) if self._filters['time']: return [(date, price) for date, price in all_prices if (date >= self._filters['time'].begin_date and date < self._filters['time'].end_date)] else: return all_prices def last_entry(self, account_name): """Get last entry of an account. Args: account_name: An account name. Returns: The last entry of the account if it is not a Close entry. """ account = realization.get_or_create(self.all_root_account, account_name) last = realization.find_last_active_posting(account.txn_postings) if last is None or isinstance(last, Close): return return get_entry(last) @property def postings(self): """All postings contained in some transaction.""" return [ posting for entry in filter_type(self.entries, Transaction) for posting in entry.postings ] def statement_path(self, entry_hash, metadata_key): """Returns the path for a statement found in the specified entry.""" entry = self.get_entry(entry_hash) value = entry.meta[metadata_key] beancount_dir = os.path.dirname(self.beancount_file_path) paths = [os.path.join(beancount_dir, value)] paths.extend([ os.path.join(beancount_dir, document_root, posting.account.replace(':', '/'), value) for posting in entry.postings for document_root in self.options['documents'] ]) for path in paths: if os.path.isfile(path): return path raise FavaAPIException('Statement not found.') def document_path(self, path): """Get absolute path of a document. Returns: The absolute path of ``path`` if it points to a document. Raises: FavaAPIException: If ``path`` is not the path of one of the documents. """ for entry in filter_type(self.entries, Document): if entry.filename == path: return path raise FavaAPIException( 'File "{}" not found in document entries.'.format(path)) def account_uptodate_status(self, account_name): """Status of the last balance or transaction. Args: account_name: An account name. Returns: A status string for the last balance or transaction of the account. - 'green': A balance check that passed. - 'red': A balance check that failed. - 'yellow': Not a balance check. """ real_account = realization.get_or_create(self.all_root_account, account_name) for txn_posting in reversed(real_account.txn_postings): if isinstance(txn_posting, Balance): if txn_posting.diff_amount: return 'red' else: return 'green' if isinstance(txn_posting, TxnPosting) and \ txn_posting.txn.flag != FLAG_UNREALIZED: return 'yellow' def account_metadata(self, account_name): """Metadata of the account. Args: account_name: An account name. Returns: Metadata of the Open entry of the account. """ real_account = realization.get_or_create(self.root_account, account_name) for posting in real_account.txn_postings: if isinstance(posting, Open): return posting.meta return {}