コード例 #1
0
ファイル: helpers.py プロジェクト: yagebu/fava
def inventory_at_dates(transactions, dates, posting_predicate):
    """Generator that yields the aggregate inventory at the specified dates.

    The inventory for a date includes all matching postings PRIOR to it.

    Args:
      transactions: list of transactions, sorted by date.
      dates: iterator of dates
      posting_predicate: predicate with the Transaction and Posting to
        decide whether to include the posting in the inventory.
    """
    if not transactions:
        return

    iterator = iter(transactions)
    txn = next(iterator)

    inventory = Inventory()

    for date in dates:
        while txn.date < date:
            for posting in txn.postings:
                if posting_predicate(posting):
                    inventory.add_position(posting)
            try:
                txn = next(iterator)
            except StopIteration:
                break
        yield inventory.get_positions()
コード例 #2
0
def balance_check(entries, options_map):
    errors = []
    tracking_accounts = set()
    for entry in entries:
        if isinstance(entry, Open):
            if entry.meta.get('tracking', False):
                tracking_accounts.add(entry.account)
    asum = Inventory()
    bsum = Inventory()
    for entry in filter_txns(entries):
        for posting in entry.postings:
            if posting.account in tracking_accounts:
                continue
            components = posting.account.split(':')
            if components[0] in ('Assets', 'Liabilities'):
                asum.add_position(posting)
            elif components[0] in ('Income', 'Expenses'):
                bsum.add_position(posting)
    csum = asum.reduce(convert.get_weight) + bsum.reduce(convert.get_weight)
    if not csum.is_small(interpolate.infer_tolerances({}, options_map)):
        errors.append(
            BudgetBalanceError(
                {
                    'filename': '<budget_balance_check>',
                    'lineno': 0
                },
                f"On-budget accounts and budget total do not match: {asum} vs {-bsum}",
                None))
    return entries, errors
コード例 #3
0
ファイル: charts.py プロジェクト: SSITB/fava
    def net_worth(self, interval):
        """Compute net worth.

        Args:
            interval: A string for the interval.

        Returns:
            A list of dicts for all ends of the given interval containing the
            net worth (Assets + Liabilities) separately converted to all
            operating currencies.
        """
        transactions = (entry for entry in self.ledger.entries
                        if (isinstance(entry, Transaction)
                            and entry.flag != flags.FLAG_UNREALIZED))

        types = (
            self.ledger.options["name_assets"],
            self.ledger.options["name_liabilities"],
        )

        txn = next(transactions, None)
        inventory = Inventory()

        for end_date_exclusive in self.ledger.interval_ends(interval):
            end_date_inclusive = end_date_exclusive - datetime.timedelta(
                days=1)
            while txn and txn.date < end_date_exclusive:
                for posting in txn.postings:
                    if posting.account.startswith(types):
                        inventory.add_position(posting)
                txn = next(transactions, None)
            yield {
                "date": end_date_exclusive,
                "balance": cost_or_value(inventory, end_date_inclusive),
            }
コード例 #4
0
def measure_balance_total(entries):
    # For timing only.
    with utils.log_time('balance_all', logging.info): # 89ms... okay.
        balance = Inventory()
        for entry in utils.filter_type(entries, data.Transaction):
            for posting in entry.postings:
                balance.add_position(posting.position)
コード例 #5
0
ファイル: helpers.py プロジェクト: gitter-badger/fava
def inventory_at_dates(transactions, dates, posting_predicate):
    """Generator that yields the aggregate inventory at the specified dates.

    The inventory for a specified date includes all matching postings PRIOR to
    it.

    :param transactions: list of transactions, sorted by date.
    :param dates: iterator of dates
    :param posting_predicate: predicate with the Transaction and Posting to
        decide whether to include the posting in the inventory.
    """
    index = 0
    length = len(transactions)

    # inventory maps lot to amount
    inventory = Inventory()
    prev_date = None
    for date in dates:
        assert prev_date is None or date > prev_date
        prev_date = date
        while index < length and transactions[index].date < date:
            entry = transactions[index]
            index += 1
            for posting in entry.postings:
                if posting_predicate(posting):
                    inventory.add_position(posting)
        yield inventory
コード例 #6
0
    def interval_totals(
        self,
        interval: Interval,
        accounts: Union[str, Tuple[str]],
        conversion: str,
    ):
        """Renders totals for account (or accounts) in the intervals.

        Args:
            interval: An interval.
            accounts: A single account (str) or a tuple of accounts.
            conversion: The conversion to use.
        """
        price_map = self.ledger.price_map
        for begin, end in pairwise(self.ledger.interval_ends(interval)):
            inventory = Inventory()
            entries = iter_entry_dates(self.ledger.entries, begin, end)
            for entry in (e for e in entries if isinstance(e, Transaction)):
                for posting in entry.postings:
                    if posting.account.startswith(accounts):
                        inventory.add_position(posting)

            yield {
                "date":
                begin,
                "balance":
                cost_or_value(inventory, conversion, price_map, end - ONE_DAY),
                "budgets":
                self.ledger.budgets.calculate_children(accounts, begin, end),
            }
コード例 #7
0
def process_account_entries(entries: data.Entries, options_map: data.Options,
                            account: Account) -> AccountData:
    """Process a single account."""
    logging.info("Processing account: %s", account)

    # Extract the relevant transactions.
    transactions = transactions_for_account(entries, account)
    if not transactions:
        logging.warning("No transactions for %s; skipping.", account)
        return transactions, None, None

    # Categorize the set of accounts encountered in the filtered transactions.
    seen_accounts = {
        posting.account
        for entry in transactions for posting in entry.postings
    }
    atypes = options.get_account_types(options_map)
    catmap = categorize_accounts(account, seen_accounts, atypes)

    # Process each of the transactions, adding derived values as metadata.
    cash_flows = []
    balance = Inventory()
    decorated_transactions = []
    for entry in transactions:

        # Update the total position in the asset we're interested in.
        positions = []
        for posting in entry.postings:
            category = catmap[posting.account]
            if category is Cat.ASSET:
                balance.add_position(posting)
                positions.append(posting)

        # Compute the signature of the transaction.
        entry = copy_and_normalize(entry)
        signature = compute_transaction_signature(catmap, entry)
        entry.meta["signature"] = signature
        entry.meta["description"] = KNOWN_SIGNATURES[signature]

        # Compute the cash flows associated with the transaction.
        flows = produce_cash_flows(entry)
        entry.meta['cash_flows'] = flows

        cash_flows.extend(
            flow._replace(balance=copy.deepcopy(balance)) for flow in flows)
        decorated_transactions.append(entry)

    currency = accountlib.leaf(account)

    cost_currencies = set(cf.amount.currency for cf in cash_flows)
    assert len(cost_currencies) == 1, str(cost_currencies)
    cost_currency = cost_currencies.pop()

    commodity_map = getters.get_commodity_directives(entries)
    comm = commodity_map[currency]

    return AccountData(account, currency, cost_currency, comm, cash_flows,
                       decorated_transactions, catmap)
コード例 #8
0
    def interval_totals(
        self,
        filtered: FilteredLedger,
        interval: Interval,
        accounts: str | tuple[str],
        conversion: str,
        invert: bool = False,
    ) -> Generator[DateAndBalanceWithBudget, None, None]:
        """Renders totals for account (or accounts) in the intervals.

        Args:
            interval: An interval.
            accounts: A single account (str) or a tuple of accounts.
            conversion: The conversion to use.
            invert: invert all numbers.
        """
        # pylint: disable=too-many-locals
        price_map = self.ledger.price_map
        for begin, end in pairwise(filtered.interval_ends(interval)):
            inventory = Inventory()
            entries = iter_entry_dates(filtered.entries, begin, end)
            account_inventories = {}
            for entry in (e for e in entries if isinstance(e, Transaction)):
                for posting in entry.postings:
                    if posting.account.startswith(accounts):
                        if posting.account not in account_inventories:
                            account_inventories[posting.account] = Inventory()
                        account_inventories[posting.account].add_position(
                            posting)
                        inventory.add_position(posting)
            balance = cost_or_value(inventory, conversion, price_map,
                                    end - ONE_DAY)
            account_balances = {}
            for account, acct_value in account_inventories.items():
                account_balances[account] = cost_or_value(
                    acct_value,
                    conversion,
                    price_map,
                    end - ONE_DAY,
                )
            budgets = {}
            if isinstance(accounts, str):
                budgets = self.ledger.budgets.calculate_children(
                    accounts, begin, end)

            if invert:
                # pylint: disable=invalid-unary-operand-type
                balance = -balance
                budgets = {k: -v for k, v in budgets.items()}
                account_balances = {k: -v for k, v in account_balances.items()}

            yield DateAndBalanceWithBudget(
                begin,
                balance,
                account_balances,
                budgets,
            )
コード例 #9
0
def compute_balance_at(transactions: data.Entries,
                       date: Optional[Date] = None) -> Inventory:
    """Compute the balance at a specific date."""
    balance = Inventory()
    for entry in transactions:
        if date is not None and entry.date >= date:
            break
        for posting in entry.postings:
            if posting.meta["category"] is Cat.ASSET:
                balance.add_position(posting)
    return balance
コード例 #10
0
 def get_inventory(self, account, date):
     inventory = Inventory()
     for entry in self.entries:
         if date is not None and entry.date > date:
             break
         if not isinstance(entry, Transaction):
             continue
         for posting in entry.postings:
             if posting.account != account:
                 continue
             inventory.add_position(get_position(posting))
     return inventory
コード例 #11
0
def SumPostings(postings: Iterable[Posting]) -> Inventory:
    """Aggregate all the positions for the given postings."""
    acc = Inventory()
    for elem in postings:
        # Note: 'elem' can be a Posting or an Inventory (the intermediate result
        # from a subcombiner).
        if isinstance(elem, Inventory):
            acc.add_inventory(elem)
        else:
            assert isinstance(elem, Posting)
            acc.add_position(elem)
    return acc
コード例 #12
0
def compute_portfolio_values(
        price_map: prices.PriceMap,
        transactions: data.Entries) -> Tuple[List[Date], List[float]]:
    """Compute a serie of portfolio values over time."""

    # Infer the list of required prices.
    currency_pairs = set()
    for entry in transactions:
        for posting in entry.postings:
            if posting.meta["category"] is Cat.ASSET:
                if posting.cost:
                    currency_pairs.add(
                        (posting.units.currency, posting.cost.currency))

    first = lambda x: x[0]
    price_dates = sorted(itertools.chain(
        ((date, None) for pair in currency_pairs
         for date, _ in prices.get_all_prices(price_map, pair)),
        ((entry.date, entry) for entry in transactions)),
                         key=first)

    # Iterate computing the balance.
    value_dates = []
    value_values = []
    balance = Inventory()
    for date, group in itertools.groupby(price_dates, key=first):
        # Update balances.
        for _, entry in group:
            if entry is None:
                continue
            for posting in entry.postings:
                if posting.meta["category"] is Cat.ASSET:
                    balance.add_position(posting)

        # Convert to market value.
        value_balance = balance.reduce(convert.get_value, price_map, date)
        cost_balance = value_balance.reduce(convert.convert_position, "USD",
                                            price_map)
        pos = cost_balance.get_only_position()
        value = pos.units.number if pos else ZERO

        # Add one data point.
        value_dates.append(date)
        value_values.append(value)

    return value_dates, value_values
コード例 #13
0
    def net_worth(
        self, interval: Interval, conversion: str
    ) -> Generator[DateAndBalance, None, None]:
        """Compute net worth.

        Args:
            interval: A string for the interval.
            conversion: The conversion to use.

        Returns:
            A list of dicts for all ends of the given interval containing the
            net worth (Assets + Liabilities) separately converted to all
            operating currencies.
        """
        transactions = (
            entry
            for entry in self.ledger.entries
            if (
                isinstance(entry, Transaction)
                and entry.flag != FLAG_UNREALIZED
            )
        )

        types = (
            self.ledger.options["name_assets"],
            self.ledger.options["name_liabilities"],
        )

        txn = next(transactions, None)
        inventory = Inventory()

        price_map = self.ledger.price_map
        for end_date in self.ledger.interval_ends(interval):
            while txn and txn.date < end_date:
                for posting in txn.postings:
                    if posting.account.startswith(types):
                        inventory.add_position(posting)
                txn = next(transactions, None)
            yield {
                "date": end_date,
                "balance": cost_or_value(
                    inventory, conversion, price_map, end_date - ONE_DAY
                ),
            }
コード例 #14
0
def tracking(entries, options_map):
    account_types = get_account_types(options_map)
    income_tracking = set()
    expense_tracking = set()

    errors = []
    new_entries = []
    for entry in entries:
        new_entry = None
        if isinstance(entry, Open) and entry.meta.get('tracking', False):
            if is_account_type(account_types.expenses, entry.account):
                expense_tracking.add(entry.account)
            elif is_account_type(account_types.income, entry.account):
                income_tracking.add(entry.account)
        elif isinstance(entry, Transaction):
            new_postings = []
            tracking_balance = Inventory()
            for posting in entry.postings:
                if 'tracking' in posting.meta:
                    new_acct = posting.meta['tracking']
                    new_posting = posting._replace(account=new_acct, meta=None)
                    new_postings.append(new_posting)
                    tracking_balance.add_position(posting)
            if new_postings:
                for position in -tracking_balance:
                    if position.units.number < 0 and len(income_tracking) == 1:
                        posting_acct, = income_tracking
                    elif position.units.number > 0 and len(
                            expense_tracking) == 1:
                        posting_acct, = expense_tracking
                    else:
                        continue
                    new_posting = Posting(posting_acct, position.units,
                                          position.cost, None, None, None)
                    new_postings.append(new_posting)

                link_id = 'tracking-' + compare.hash_entry(entry)
                new_links = entry.links | set([link_id])
                entry = entry._replace(links=new_links)
                new_entry = entry._replace(postings=new_postings)
        new_entries.append(entry)
        if new_entry:
            new_entries.append(new_entry)
    return new_entries, errors
コード例 #15
0
    def interval_totals(
        self,
        interval: Interval,
        accounts: Union[str, Tuple[str]],
        conversion: str,
        invert: bool = False,
    ) -> Generator[DateAndBalanceWithBudget, None, None]:
        """Renders totals for account (or accounts) in the intervals.

        Args:
            interval: An interval.
            accounts: A single account (str) or a tuple of accounts.
            conversion: The conversion to use.
        """
        price_map = self.ledger.price_map
        for begin, end in pairwise(self.ledger.interval_ends(interval)):
            inventory = Inventory()
            entries = iter_entry_dates(self.ledger.entries, begin, end)
            for entry in (e for e in entries if isinstance(e, Transaction)):
                for posting in entry.postings:
                    if posting.account.startswith(accounts):
                        inventory.add_position(posting)

            balance = cost_or_value(
                inventory, conversion, price_map, end - ONE_DAY
            )
            budgets = {}
            if isinstance(accounts, str):
                budgets = self.ledger.budgets.calculate_children(
                    accounts, begin, end
                )

            if invert:
                # pylint: disable=invalid-unary-operand-type
                balance = -balance
                budgets = {k: -v for k, v in budgets.items()}

            yield {
                "date": begin,
                "balance": balance,
                "budgets": budgets,
            }
コード例 #16
0
ファイル: charts.py プロジェクト: TomJohnZ/fava
    def interval_totals(self, interval, accounts):
        """Renders totals for account (or accounts) in the intervals.

        Args:
            interval: A string for the interval.
            accounts: A single account (str) or a tuple of accounts.
        """
        for begin, end in pairwise(self.ledger.interval_ends(interval)):
            inventory = Inventory()
            entries = iter_entry_dates(self.ledger.entries, begin, end)
            for entry in filter_type(entries, Transaction):
                for posting in entry.postings:
                    if posting.account.startswith(accounts):
                        inventory.add_position(posting)

            yield {
                'begin_date': begin,
                'totals': _inventory_cost_or_value(inventory, end),
                'budgets':
                self.ledger.budgets.calculate(accounts[0], begin, end),
            }
コード例 #17
0
def merge_postings(account: Account, postings: List[Posting], meta_name: Union[str, None]) -> List[Posting]:
    """
    Merges postings with an equal account name and takes meta of the first one.
    If `meta_name` is provided, then in a way of metaset combine meta values whose keys equal to `meta_name`.

    Args:
      postings: a list of postings.
      account: an account name whose postings should be merged.
      meta_name: a key for meta values that should be combined in a way of metaset.

    Example:

    Returns:
      A list of postings.
    """
    grouped_postings = []
    share_postings = []
    share_balance = Inventory()
    meta = dict()
    for posting in postings:
        if posting.account == account:
            share_postings.append(posting)
            share_balance.add_position(posting)
            if len(meta) == 0:
                meta = posting.meta
            elif meta_name is not None:
                for mark in metaset.get(posting.meta, meta_name):
                    share_postings[0] = share_postings[0]._replace(
                        meta=metaset.add(share_postings[0].meta, meta_name, mark)
                    )
        else:
            grouped_postings.append(posting)

    if share_postings:
        for pos in share_balance:
            grouped_postings.append(Posting(account, pos.units, pos.cost, None, None, share_postings[0].meta))

    return grouped_postings
コード例 #18
0
ファイル: charts.py プロジェクト: mhansen/fava
    def net_worth(self, interval):
        """Compute net worth.

        Args:
            interval: A string for the interval.

        Returns:
            A list of dicts for all ends of the given interval containing the
            net worth (Assets + Liabilities) separately converted to all
            operating currencies.
        """
        transactions = (entry for entry in self.ledger.entries
                        if (isinstance(entry, Transaction)
                            and entry.flag != flags.FLAG_UNREALIZED))

        types = (self.ledger.options['name_assets'],
                 self.ledger.options['name_liabilities'])

        txn = next(transactions, None)
        inventory = Inventory()

        for date in self.ledger.interval_ends(interval):
            while txn and txn.date < date:
                for posting in filter(lambda p: p.account.startswith(types),
                                      txn.postings):
                    inventory.add_position(posting)
                txn = next(transactions, None)
            yield {
                'date': date,
                'balance': {
                    currency:
                    inventory.reduce(convert.convert_position, currency,
                                     self.ledger.price_map,
                                     date).get_currency_units(currency).number
                    for currency in self.ledger.options['operating_currency']
                }
            }
コード例 #19
0
ファイル: holdings.py プロジェクト: tichai/fava
def inventory_at_dates(transactions, dates, posting_predicate):
    """Generator that yields the aggregate inventory at the specified dates.

    The inventory for a date includes all matching postings PRIOR to it.

    Args:
      transactions: list of transactions, sorted by date.
      dates: iterator of dates
      posting_predicate: predicate with the Transaction and Posting to
        decide whether to include the posting in the inventory.
    """

    iterator = iter(transactions)
    txn = next(iterator, None)

    inventory = Inventory()

    for date in dates:
        while txn and txn.date < date:
            for posting in txn.postings:
                if posting_predicate(posting):
                    inventory.add_position(posting)
            txn = next(iterator, None)
        yield inventory.get_positions()
コード例 #20
0
def compute_entries_balance(entries, prefix=None, date=None):
    """Compute the balance of all postings of a list of entries.

    Sum up all the positions in all the postings of all the transactions in the
    list of entries and return an inventory of it.

    Args:
      entries: A list of directives.
      prefix: If specified, a prefix string to restrict by account name. Only
        postings with an account that starts with this prefix will be summed up.
      date: A datetime.date instance at which to stop adding up the balance.
        The date is exclusive.
    Returns:
      An instance of Inventory.
    """
    total_balance = Inventory()
    for entry in entries:
        if not (date is None or entry.date < date):
            break
        if isinstance(entry, Transaction):
            for posting in entry.postings:
                if prefix is None or posting.account.startswith(prefix):
                    total_balance.add_position(posting)
    return total_balance
コード例 #21
0
 def test_add_position(self):
     inv = Inventory()
     for pos in self.POSITIONS_ALL_KINDS:
         inv.add_position(pos)
     self.assertEqual(Inventory(self.POSITIONS_ALL_KINDS), inv)
コード例 #22
0
        new_inventory = Inventory()
        do_round = True
        for position in inventory:
            # units_digits = get_digits(position.units.number)
            # cost_digits = get_digits(position.cost.number)
            units_digits = 4
            cost_digits = 4
            new_units_number = position.units.number * new_to_old_ratio
            new_cost_number = position.cost.number / new_to_old_ratio
            if do_round:
                new_units_number = round(new_units_number, units_digits)
                new_cost_number = round(new_cost_number, cost_digits)
            new_position = Position(units=Amount(new_units_number,
                                                 new_currency),
                                    cost=Cost(number=new_cost_number,
                                              currency=position.cost.currency,
                                              date=position.cost.date,
                                              label=position.cost.label))
            new_inventory.add_position(new_position)
            print('  %s %s' % (args.transfer_to, new_position))
            print('  %s %s' % (args.account, -position))
        print('New units: ', new_inventory.reduce(get_units))
        print('New cost: ', new_inventory.reduce(get_cost))
    elif args.transfer_to:
        for position in inventory:
            print('  %s %s' % (args.transfer_to, position))
            print('  %s %s' % (args.account, -position))
    else:
        for position in inventory:
            print('  %s %s' % (args.account, position))
コード例 #23
0
def sum_income(tx: Transaction) -> Inventory:
    total = Inventory()
    for posting in tx.postings:
        if posting.account.split(":")[0] == "Income":
            total.add_position(posting)
    return total
コード例 #24
0
def sum_expenses(tx: Transaction) -> Inventory:
    total = Inventory()
    for posting in tx.postings:
        if posting.account.split(":")[0] == "Expenses":
            total.add_position(posting)
    return total
コード例 #25
0
def process_account_entries(entries: data.Entries, config: InvestmentConfig,
                            investment: Investment,
                            check_explicit_flows: bool) -> AccountData:
    """Process a single account."""
    account = investment.asset_account
    logging.info("Processing account: %s", account)

    # Extract the relevant transactions.
    transactions = extract_transactions_for_account(entries, investment)
    if not transactions:
        logging.warning("No transactions for %s; skipping.", account)
        return None

    # Categorize the set of accounts encountered in the filtered transactions.
    seen_accounts = {
        posting.account
        for entry in transactions for posting in entry.postings
    }
    catmap = categorize_accounts(config, investment, seen_accounts)

    # Process each of the transactions, adding derived values as metadata.
    cash_flows = []
    balance = Inventory()
    decorated_transactions = []
    for entry in transactions:

        # Compute the signature of the transaction.
        entry = categorize_entry(catmap, entry)
        signature = compute_transaction_signature(entry)
        entry.meta["signature"] = signature

        # TODO(blais): Cache balance in every transaction to speed up
        # computation? Do this later.
        if False:
            # Update the total position in the asset we're interested in.
            for posting in entry.postings:
                if posting.meta["category"] is Cat.ASSET:
                    balance.add_position(posting)

        # Compute the cash flows associated with the transaction.
        flows_general = produce_cash_flows_general(entry, account)
        if check_explicit_flows:
            # Attempt the explicit method.
            flows_explicit = produce_cash_flows_explicit(entry, account)
            if flows_explicit != flows_general:
                print(
                    "Differences found between general and explicit methods:")
                print("Explicit handlers:")
                for flow in flows_explicit:
                    print("  ", flow)
                print("General handler:")
                for flow in flows_general:
                    print("  ", flow)
                raise ValueError(
                    "Differences found between general and explicit methods:")

        cash_flows.extend(flows_general)
        decorated_transactions.append(entry)

    cost_currencies = set(cf.amount.currency for cf in cash_flows)
    #assert len(cost_currencies) == 1, str(cost_currencies)
    cost_currency = cost_currencies.pop() if cost_currencies else None

    currency = investment.currency
    commodity_map = getters.get_commodity_directives(entries)
    comm = commodity_map[currency] if currency else None

    open_close_map = getters.get_account_open_close(entries)
    opn, cls = open_close_map[account]

    # Compute the final balance.
    balance = compute_balance_at(decorated_transactions)

    return AccountData(account, currency, cost_currency, comm, opn, cls,
                       cash_flows, decorated_transactions, balance, catmap)