コード例 #1
0
    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()
コード例 #2
0
ファイル: __init__.py プロジェクト: yegle/fava
    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()
コード例 #3
0
    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()
コード例 #4
0
    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()
コード例 #5
0
ファイル: __init__.py プロジェクト: TomJohnZ/fava
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 {}