Exemple #1
0
def book_price_conversions_plugin(entries, options_map, config):
    """Plugin that rewrites transactions to insert cost basis according to a booking method.

    See book_price_conversions() for details.

    Args:
      entries: A list of entry instances.
      options_map: A dict of options parsed from the file.
      config: A string, in "<ACCOUNT1>,<ACCOUNT2>" format.
    Returns:
      A tuple of
        entries: A list of new, modified entries.
        errors: A list of errors generated by this plugin.
    """
    # The expected configuration is two account names, separated by whitespace.
    errors = []
    try:
        assets_account, income_account = re.split(r'[,; \t]', config)
        if not account.is_valid(assets_account) or not account.is_valid(
                income_account):
            raise ValueError("Invalid account string")
    except ValueError as exc:
        errors.append(
            ConfigError(
                None,
                ('Invalid configuration: "{}": {}, skipping booking').format(
                    config, exc), None))
        return entries, errors

    new_entries, errors, _ = book_price_conversions(entries, assets_account,
                                                    income_account)
    return new_entries, errors
Exemple #2
0
    def __init__(self, **kwds):
        """Pull 'filing' and 'prefix' from kwds."""

        self.filing_account = kwds.pop('filing', None)
        assert account.is_valid(self.filing_account)

        self.prefix = kwds.pop('prefix', None)
        assert account.is_valid(self.filing_account)

        super().__init__(**kwds)
    def __init__(
            self,
            *args,
            currency=None,
            reimbursement_account=None,
            discount_account=None,
            prefix=None,
            tags=set(),
            debug=False,
            **kwargs):
        '''
        Available keyword arguments:
            reimbursement_account   the income account to use for
                             reimbursements and filing (unless filing is
                             passed in as a keyword argument).
            discount_account the income account to use for discounts.
            currency         the currency for all transactions (defaults
                             to "USD").
            tags             a set of tags to add to every transaction.
            prefix           the filename prefix to use when beancount-file
                             moves files (defaults to "UHC").
            debug            if True, every row will be printed to stdout.
        Additional keyword arguments for IdentifyMixin and FilingMixin are
        permitted. Most significantly:
            matchers         a list of 2-tuples, where the first item is one of
                             "mime", "filename", or "content"; and the second
                             is a regular expression which, if matched,
                             identifies a file as one for which this importer
                             should be used.
        '''
        self.reimbursement_account = reimbursement_account \
            or self.reimbursement_account
        self.discount_account = discount_account or self.discount_account
        self.currency = currency or self.currency
        self.tags.update(tags)

        if debug is not None:
            self.debug = debug

        assert beancount_account.is_valid(self.reimbursement_account),\
            "{} is not a valid account".format(self.reimbursement_account)
        assert beancount_account.is_valid(self.discount_account),\
            "{} is not a valid account".format(self.discount_account)

        super().__init__(
            *args,
            prefix=prefix or self.prefix,
            filing=kwargs.get('filing', None) or self.reimbursement_account,
            row_fields=row_fields,
            row_class=Row,
            **kwargs
        )
    def __init__(
            self,
            *args,
            account=None,
            currency=None,
            prefix=None,
            tags=set(),
            debug=False,
            invert_amounts=None,
            **kwargs):
        '''
        Available keyword arguments:
            account         the account to use for every transaction.
            currency        the account currency (defaults to "USD").
            tags            a set of tags to add to every transaction.
            prefix          the filename prefix to use when beancount-file
                            moves files.
            debug           if True, every row will be printed to stdout.
        Additional keyword arguments for CategorizerMixin, IdentifyMixin and
        FilingMixin are permitted. Most significantly:
            categorizers    a list of callables, taking one argument, the
                            csv row (as a Row namedtuple object). The callable
                            may return a CategorizerResult, which will set
                            fields on the transaction; or a string being the
                            name of an account against which to offset the
                            entire amount of the transaction; or None, in which
                            case it will be ignored. If more than one
                            categorizer returns a non-None result, the first
                            result will be used and the transaction will be
                            flagged.
            matchers        a list of 2-tuples, where the first item is one of
                            "mime", "filename", or "content"; and the second is
                            a regular expression which, if matched, identifies
                            a file as one for which this importer should be
                            used.
        '''
        assert self.row_class is not None,\
            "The row_class property must be defined."

        self.account = account or self.account
        self.currency = currency or self.currency
        self.tags.update(tags)

        if invert_amounts is not None:
            self.invert_amounts = invert_amounts

        if debug is not None:
            self.debug = debug

        assert beancount_account.is_valid(self.account),\
            "{} is not a valid account".format(self.account)

        super().__init__(
            *args,
            prefix=prefix or self.prefix,
            filing=self.account,
            row_fields=self.row_fields,
            row_class=self.row_class,
            **kwargs
        )
def C(pattern,
      account_name,
      field_name='description',
      row_must_have_field=True,
      **kwargs):
    '''
    Make a categorizer that returns the given account when the given regular
    expression is matched on the row's $field_name field (defaults to
    description). All CategorizerResult fields except account are valid keyword
    arguments.
    '''

    assert 'account' not in kwargs,\
        "'account' is not permitted as a keyword argument"

    assert account.is_valid(account_name),\
        "{} is not a valid account".format(account_name)

    def f(row):
        row = row._asdict()
        if row_must_have_field:
            assert field_name in row,\
                f'Row used for categorizer must have a {field_name} field'

        if re.search(pattern, row.get(field_name, ''),
                     re.IGNORECASE) is not None:
            return CategorizerResult(account=account_name, **kwargs)
        return None

    return f
Exemple #6
0
    def get_account(self, row):
        result = self.fees_categorizer(row)
        if result is not None:
            if not isinstance(result, CategorizerResult):
                result = CategorizerResult(account=result)
            assert account.is_valid(result.account),\
                "{} is not a valid account".format(result.account)
            return result.account

        return self.account
Exemple #7
0
    def __init__(self, **kwds):
        """Pull 'filing' and 'prefix' from kwds.

        Args:
          filing: The name of the account to file to.
          prefix: The name of the institution prefix to insert.
        """

        self.filing_account = kwds.pop('filing', None)
        assert account.is_valid(self.filing_account)

        self.prefix = kwds.pop('prefix', None)

        super().__init__(**kwds)
Exemple #8
0
    def get_categorized_postings(self, row):
        postings = [
            data.Posting(
                account=self.get_account(row),
                units=self.get_amount(row),
                cost=None,
                price=self.get_price(row),
                flag=None,
                meta={}),
        ]

        final_result = None

        for result in [c(row) for c in self.categorizers]:
            if result is None:
                continue

            if final_result is not None:
                # Uh oh, more than one thing matched
                final_result = final_result._replace(flag=flags.FLAG_WARNING)
                break

            if not isinstance(result, CategorizerResult):
                result = CategorizerResult(account=result)

            assert account.is_valid(result.account),\
                "{} is not a valid account".format(result.account)

            final_result = result

            postings.append(data.Posting(
                account=result.account,
                units=-self.get_amount(row),
                cost=None,
                price=self.get_price(row),
                flag=None,
                meta={}))

        return postings, final_result
Exemple #9
0
def fill_account(entries, unused_options_map, insert_account):
    """Insert an posting with a default account when there is only a single posting.

    Args:
      entries: A list of directives.
      unused_options_map: A parser options dict.
      insert_account: A string, the name of the account.
    Returns:
      A list of entries, possibly with more Price entries than before, and a
      list of errors.
    """
    if not account.is_valid(insert_account):
        return entries, [
            FillAccountError(
                data.new_metadata('<fill_account>', 0),
                "Invalid account name: '{}'".format(insert_account), None)
        ]

    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction) and len(entry.postings) == 1:
            inv = inventory.Inventory()
            for posting in entry.postings:
                if posting.cost is None:
                    inv.add_amount(posting.units)
                else:
                    inv.add_amount(convert.get_cost(posting))
            inv.reduce(convert.get_units)
            new_postings = list(entry.postings)
            for pos in inv:
                new_postings.append(
                    data.Posting(insert_account, -pos.units, None, None, None,
                                 None))
            entry = entry._replace(postings=new_postings)
        new_entries.append(entry)

    return new_entries, []
def insert_currency_trading_postings(entries, options_map, config):
    """Insert currency trading postings.

    Args:
      entries: A list of directives.
      unused_options_map: An options map.
      config: The base account name for currency trading accounts.
    Returns:
      A list of new errors, if any were found.
    """
    base_account = config.strip()
    if not account.is_valid(base_account):
        base_account = DEFAULT_BASE_ACCOUNT

    errors = []
    new_entries = []
    new_accounts = set()
    for entry in entries:
        if isinstance(entry, Transaction):
            curmap, has_price = group_postings_by_weight_currency(entry)
            if has_price and len(curmap) > 1:
                new_postings = get_neutralizing_postings(
                    curmap, base_account, new_accounts)
                entry = entry._replace(postings=new_postings)
                if META_PROCESSED:
                    entry.meta[META_PROCESSED] = True
        new_entries.append(entry)

    earliest_date = entries[0].date
    open_entries = [
        data.Open(data.new_metadata('<currency_accounts>', index),
                  earliest_date, acc, None, None)
        for index, acc in enumerate(sorted(new_accounts))
    ]

    return open_entries + new_entries, errors
Exemple #11
0
def add_unrealized_gains(entries, options_map, subaccount=None):
    """Insert entries for unrealized capital gains.

    This function inserts entries that represent unrealized gains, at the end of
    the available history. It returns a new list of entries, with the new gains
    inserted. It replaces the account type with an entry in an income account.
    Optionally, it can book the gain in a subaccount of the original and income
    accounts.

    Args:
      entries: A list of data directives.
      options_map: A dict of options, that confirms to beancount.parser.options.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of entries, which includes the new unrealized capital gains entries
      at the end, and a list of errors. The new list of entries is still sorted.
    """
    errors = []
    meta = data.new_metadata('<unrealized_gains>', 0)

    account_types = options.get_account_types(options_map)

    # Assert the subaccount name is in valid format.
    if subaccount:
        validation_account = account.join(account_types.assets, subaccount)
        if not account.is_valid(validation_account):
            errors.append(
                UnrealizedError(meta,
                                "Invalid subaccount name: '{}'".format(subaccount),
                                None))
            return entries, errors

    if not entries:
        return (entries, errors)

    # Group positions by (account, cost, cost_currency).
    price_map = prices.build_price_map(entries)
    holdings_list = holdings.get_final_holdings(entries, price_map=price_map)

    # Group positions by (account, cost, cost_currency).
    holdings_list = holdings.aggregate_holdings_by(
        holdings_list, lambda h: (h.account, h.currency, h.cost_currency))

    # Get the latest prices from the entries.
    price_map = prices.build_price_map(entries)

    # Create transactions to account for each position.
    new_entries = []
    latest_date = entries[-1].date
    for index, holding in enumerate(holdings_list):
        if (holding.currency == holding.cost_currency or
            holding.cost_currency is None):
            continue

        # Note: since we're only considering positions held at cost, the
        # transaction that created the position *must* have created at least one
        # price point for that commodity, so we never expect for a price not to
        # be available, which is reasonable.
        if holding.price_number is None:
            # An entry without a price might indicate that this is a holding
            # resulting from leaked cost basis. {0ed05c502e63, b/16}
            if holding.number:
                errors.append(
                    UnrealizedError(meta,
                                    "A valid price for {h.currency}/{h.cost_currency} "
                                    "could not be found".format(h=holding), None))
            continue

        # Compute the PnL; if there is no profit or loss, we create a
        # corresponding entry anyway.
        pnl = holding.market_value - holding.book_value
        if holding.number == ZERO:
            # If the number of units sum to zero, the holdings should have been
            # zero.
            errors.append(
                UnrealizedError(
                    meta,
                    "Number of units of {} in {} in holdings sum to zero "
                    "for account {} and should not".format(
                        holding.currency, holding.cost_currency, holding.account),
                    None))
            continue

        # Compute the name of the accounts and add the requested subaccount name
        # if requested.
        asset_account = holding.account
        income_account = account.join(account_types.income,
                                      account.sans_root(holding.account))
        if subaccount:
            asset_account = account.join(asset_account, subaccount)
            income_account = account.join(income_account, subaccount)

        # Create a new transaction to account for this difference in gain.
        gain_loss_str = "gain" if pnl > ZERO else "loss"
        narration = ("Unrealized {} for {h.number} units of {h.currency} "
                     "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, "
                     "average cost: {h.cost_number:.4f} {h.cost_currency})").format(
                         gain_loss_str, h=holding)
        entry = data.Transaction(data.new_metadata(meta["filename"], lineno=1000 + index),
                                 latest_date, flags.FLAG_UNREALIZED,
                                 None, narration, EMPTY_SET, EMPTY_SET, [])

        # Book this as income, converting the account name to be the same, but as income.
        # Note: this is a rather convenient but arbitrary choice--maybe it would be best to
        # let the user decide to what account to book it, but I don't a nice way to let the
        # user specify this.
        #
        # Note: we never set a price because we don't want these to end up in Conversions.
        entry.postings.extend([
            data.Posting(
                asset_account,
                amount.Amount(pnl, holding.cost_currency),
                None,
                None,
                None,
                None),
            data.Posting(
                income_account,
                amount.Amount(-pnl, holding.cost_currency),
                None,
                None,
                None,
                None)
        ])

        new_entries.append(entry)

    # Ensure that the accounts we're going to use to book the postings exist, by
    # creating open entries for those that we generated that weren't already
    # existing accounts.
    new_accounts = {posting.account
                    for entry in new_entries
                    for posting in entry.postings}
    open_entries = getters.get_account_open_close(entries)
    new_open_entries = []
    for account_ in sorted(new_accounts):
        if account_ not in open_entries:
            meta = data.new_metadata(meta["filename"], index)
            open_entry = data.Open(meta, latest_date, account_, None, None)
            new_open_entries.append(open_entry)

    return (entries + new_open_entries + new_entries, errors)
Exemple #12
0
def auto_depreciation(entries, options_map, config=None):
    """Depreciate fixed assets automatically.

    Please refer to `Beancount Scripting & Plugins <http://furius.ca/beancount/doc/scripting>`_
    for more details.
    
    Parameters
    ----------
    entries
        A list of entry instance.
    options_map
        A dict of options parsed from the file.
    config : str
        A string of plugin configuration.
    
    Returns
    -------
    entries
        A list of processed entries.
    errors
        Error information.
    """
    errors = []
    DEFAULT_ASSETS_ACCOUNT = 'Assets:Wealth:Fixed-Assets'
    DEFAULT_EXPENSES_ACCOUNT = 'Expenses:Property-Expenses:Depreciation'
    DEFAULT_METHOD = 'parabola'
    DEFAULT_RESIDUAL_VALUE = 0.0
    try:
        config_dict = eval(config)
    except (TypeError, SyntaxError):
        config_dict = {}
    try:
        assets_account = config_dict['assets']
    except KeyError:
        assets_account = DEFAULT_ASSETS_ACCOUNT
    if not account.is_valid(assets_account):
        assets_account = DEFAULT_ASSETS_ACCOUNT
    try:
        expenses_account = config_dict['expenses']
    except KeyError:
        expenses_account = DEFAULT_EXPENSES_ACCOUNT
    if not account.is_valid(expenses_account):
        expenses_account = DEFAULT_EXPENSES_ACCOUNT
    try:
        method = config_dict['method']
    except KeyError:
        method = DEFAULT_METHOD

    depreciation_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction):
            for posting in entry.postings:
                if (posting.meta and 'useful_life' in posting.meta
                        and posting.account == assets_account):
                    cost = posting.cost
                    currency = cost.currency
                    original_value = float(cost.number)
                    try:
                        end_value = float(posting.meta['residual_value'])
                    except KeyError:
                        end_value = DEFAULT_RESIDUAL_VALUE
                    label = cost.label
                    buy_date = cost.date
                    m = re.match(r'([0-9]+)([my])',
                                 str.lower(posting.meta['useful_life']))
                    months_or_years = m.group(2)
                    months = int(m.group(1))
                    if months_or_years == 'y':
                        months = 12 * months
                    dates_list, current_values, depreciation_values \
                        = depreciation_list(original_value, end_value, buy_date, months, method)
                    latest_pos = posting
                    for i, date in enumerate(dates_list):
                        pos_sell = _posting_to_sell(latest_pos)
                        pos_buy = _posting_to_buy(latest_pos, date,
                                                  current_values[i])
                        pos_expense = _posting_to_expense(
                            latest_pos, expenses_account,
                            depreciation_values[i], currency)
                        latest_pos = pos_buy
                        new_pos = [pos_sell, pos_buy, pos_expense]
                        depreciation_entries.append(
                            _auto_entry(entry, date, label, *new_pos))
    new_entries = entries + depreciation_entries
    new_entries.sort(key=data.entry_sortkey)
    return new_entries, errors
Exemple #13
0
 def test_is_valid(self):
     self.assertTrue(account.is_valid("Assets:US:RBS:Checking"))
     self.assertTrue(account.is_valid("Equity:Opening-Balances"))
     self.assertTrue(account.is_valid("Income:US:ETrade:Dividends-USD"))
     self.assertTrue(account.is_valid("Assets:US:RBS"))
     self.assertTrue(account.is_valid("Assets:US"))
     self.assertFalse(account.is_valid("Assets"))
     self.assertFalse(account.is_valid("Invalid"))
     self.assertFalse(account.is_valid("Other"))
     self.assertFalse(account.is_valid("Assets:US:RBS*Checking"))
     self.assertFalse(account.is_valid("Assets:US:RBS:Checking&"))
     self.assertFalse(account.is_valid("Assets:US:RBS:checking"))
     self.assertFalse(account.is_valid("Assets:us:RBS:checking"))
Exemple #14
0
def make_records(*, transaction_details: TransactionDetailsForAddresses,
                 addresses: list[Address], transactions: list[Transactions],
                 currency: spotbit.CurrencyName) -> str:

    assert transaction_details
    assert addresses
    assert transactions
    assert currency

    # FIXME I can't use the beancount api to create a data structure that I can then dump to a beancount file.
    # Instead I must emit strings then load the strings to test for correctness.

    # Ref. https://beancount.github.io/docs/beancount_language_syntax.html
    # FINDOUT How to create a collection of entries and dump it to text file.
    # - Create accounts
    # - Create transactions
    # - Add postings to transactions.
    # - Dump the account to a file.

    # Ref. realization.py
    type = 'Assets'
    country = ''
    institution = ''
    btc_account_name = 'BTC'
    fiat_account_name = currency.value
    subaccount_name = ''

    from beancount.core import account

    components = [
        type.title(),
        country.title(),
        institution.title(), btc_account_name, subaccount_name
    ]
    components = [c for c in components if c != '']
    btc_account = account.join(*components)
    assert account.is_valid(
        btc_account), f'Account name is not valid. Got: {btc_account}'

    # Test: Treat cash as a liability.
    # TODO(nochiel) Store the exchange rate at each transaction date.
    components = [
        'liabilities'.title(), 'Cash', fiat_account_name, subaccount_name
    ]
    components = [c for c in components if c != '']
    fiat_account = account.join(*components)
    assert account.is_valid(
        fiat_account), f'Account name is not valid. Got: {fiat_account}'

    # Loop through on-chain transactions and create transactions and relevant postings for each transaction.

    def get_earliest_blocktime(
            transactions: list[Transactions] = transactions) -> datetime:
        assert transactions

        result = datetime.now()
        if transactions[0]:
            result = datetime.fromtimestamp(
                transactions[0][0]['status']['block_time'])

        for transactions_for in transactions:
            if transactions_for:
                for transaction in transactions_for:
                    timestamp = datetime.fromtimestamp(
                        transaction['status']['block_time'])
                    result = timestamp if timestamp < result else result

        return result

    date_of_account_open = get_earliest_blocktime().date()

    # Commodity directive
    '''
    1867-07-01 commodity CAD
      name: "Canadian Dollar"
      asset-class: "cash"
    '''
    btc_commodity_directive = ('2008-10-31 commodity BTC\n'
                               '  name: "Bitcoin"\n'
                               '  asset-class: "cryptocurrency"\n')

    # Account directive
    # e.g. YYYY-MM-DD open Account [ConstraintCurrency,...] ["BookingMethod"]
    account_directives = [
        f'{date_of_account_open} open {btc_account}\tBTC',
        f'{date_of_account_open} open {fiat_account}\t{currency.value}',
    ]

    transactions_by_hash = {
        tx['txid']: tx
        for for_address in transactions for tx in for_address
    }
    '''
    transaction_details_by_hash = {detail.hash : detail
            for details_for in transaction_details.values()
            for detail in details_for}

    n_inputs = 0
    for d in transaction_details_by_hash.values():
        if d.is_input: n_inputs += 1 

    logger.debug(f'Number of input txs: {n_inputs}')
    logger.debug(f'Number of all txs: {len(transaction_details_by_hash.values())}')
    '''

    # TODO(nochiel) Order transactions by date. For each date record a btc price.
    # e.g. 2015-04-30 price AAPL 125.15 USD
    btc_price_directive = ''

    # Transactions and entries. e.g.
    '''
    2014-05-05 * "Using my new credit card"
      Liabilities:CreditCard:CapitalOne         -37.45 USD
      Expenses:Restaurant

    2014-02-03 * "Initial deposit"
    Assets:US:BofA:Checking         100 USD
    Assets:Cash                    -100 USD
    '''

    # TODO Should I generate Expense accounts if there is an output address that's re-used?
    # TODO Create an Asset:BTC and Asset:Cash account
    # The Asset:Cash is equivalent to Asset:BTC but in USD exchange rates at the time of transaction.
    # Because BTC is volatile, we should list transaction time.

    class Payee:
        def __init__(self, address: str, amount: int):
            self.address = address
            self.amount = amount  # satoshis

    Transaction = dict

    def get_payees(transaction: Transaction, ) -> list[Payee]:

        assert Transaction

        result = []

        outputs = transaction['vout']
        result = [
            Payee(address=output['scriptpubkey_address'],
                  amount=output['value']) for output in outputs
        ]

        return result

    assert transaction_details
    # logger.debug(f'transaction_details: {transaction_details}')

    transaction_directives = []
    for address in addresses:

        details = transaction_details[address]
        # Post transactions in chronological order. Esplora gives us reverse-chronological order
        details.reverse()

        for detail in details:

            # Create a beancount transaction for each transaction.
            # Then add beancount transaction entries for each payee/output that is not one of the user's addresses.
            ''' E.g. 
                2014-07-11 * "Sold shares of S&P 500"
                  Assets:ETrade:IVV               -10 IVV {183.07 USD} @ 197.90 USD
                  Assets:ETrade:Cash          1979.90 USD
                  Income:ETrade:CapitalGains
                '''
            meta = ''
            date = detail.timestamp.date()
            flag = '*'
            payees = get_payees(transactions_by_hash[detail.hash])
            if not detail.is_input:
                payees_in_descriptor = filter(
                    lambda payee: payee.address in addresses, payees)
                payees = list(payees_in_descriptor)

            tags = []
            links = []

            # Should a payee posting use the output address as a subaccount?
            # Each payee is a transaction
            # If not is_input put our receiving transactions first.
            for payee in payees:
                transaction_directive = f'{date} * "{payee.address}" "Transaction hash: {detail.hash}"'

                btc_payee_transaction_directive = f'\t{btc_account}\t{"-" if detail.is_input else ""}{payee.amount * 1e-8 : .8f} BTC'

                transaction_fiat_amount = detail.twap * payee.amount * 1e-8
                if not detail.is_input:
                    btc_payee_transaction_directive += f' {{{detail.twap : .2f} {currency.value} }}'
                if detail.is_input:
                    btc_payee_transaction_directive += f' @ {detail.twap : .2f} {currency.value}\t'
                fiat_payee_transaction_directive = (
                    f'\t{fiat_account}\t{"-" if not detail.is_input else ""}' +
                    f'{transaction_fiat_amount : .2f} {currency.value}\t')

                payee_transaction_directive = btc_payee_transaction_directive
                payee_transaction_directive += '\n'
                payee_transaction_directive += fiat_payee_transaction_directive

                transaction_directive += '\n'
                transaction_directive += payee_transaction_directive
                transaction_directive += '\n'
                transaction_directives.append(transaction_directive)

    document = ''
    document = btc_commodity_directive
    document += '\n'
    document += str.join('\n', account_directives)
    document += '\n\n'
    document += str.join('\n', transaction_directives)

    # TODO Validate document
    from beancount import loader
    _, errors, _ = loader.load_string(document)
    if errors:
        logger.error(
            f'---{len(errors)} Errors in the generated beancount file---')
        for error in errors:
            logger.error(error)

    return document
Exemple #15
0
def add_unrealized_gains(entries, options_map, subaccount=None):
    """Insert entries for unrealized capital gains.

    This function inserts entries that represent unrealized gains, at the end of
    the available history. It returns a new list of entries, with the new gains
    inserted. It replaces the account type with an entry in an income account.
    Optionally, it can book the gain in a subaccount of the original and income
    accounts.

    Args:
      entries: A list of data directives.
      options_map: A dict of options, that confirms to beancount.parser.options.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of entries, which includes the new unrealized capital gains entries
      at the end, and a list of errors. The new list of entries is still sorted.
    """
    errors = []
    meta = data.new_metadata('<unrealized_gains>', 0)

    account_types = options.get_account_types(options_map)

    # Assert the subaccount name is in valid format.
    if subaccount:
        validation_account = account.join(account_types.assets, subaccount)
        if not account.is_valid(validation_account):
            errors.append(
                UnrealizedError(
                    meta, "Invalid subaccount name: '{}'".format(subaccount),
                    None))
            return entries, errors

    if not entries:
        return (entries, errors)

    # Group positions by (account, cost, cost_currency).
    price_map = prices.build_price_map(entries)

    new_entries = []

    # Start at the first month after our first transaction
    date = date_utils.next_month(entries[0].date)
    last_month = date_utils.next_month(entries[-1].date)
    last_holdings_with_currencies = None
    while date <= last_month:
        date_entries, holdings_with_currencies, date_errors = add_unrealized_gains_at_date(
            entries, new_entries, account_types.income, price_map, date, meta,
            subaccount)
        new_entries.extend(date_entries)
        errors.extend(date_errors)

        if last_holdings_with_currencies:
            for account_, cost_currency, currency in last_holdings_with_currencies - holdings_with_currencies:
                # Create a negation transaction specifically to mark that all gains have been realized
                if subaccount:
                    account_ = account.join(account_, subaccount)

                latest_unrealized_entry = find_previous_unrealized_transaction(
                    new_entries, account_, cost_currency, currency)
                if not latest_unrealized_entry:
                    continue
                entry = data.Transaction(
                    data.new_metadata(meta["filename"],
                                      lineno=999,
                                      kvlist={'prev_currency': currency}),
                    date, flags.FLAG_UNREALIZED, None,
                    'Clear unrealized gains/losses of {}'.format(currency),
                    set(), set(), [])

                # Negate the previous transaction because of unrealized gains are now 0
                for posting in latest_unrealized_entry.postings[:2]:
                    entry.postings.append(
                        data.Posting(posting.account, -posting.units, None,
                                     None, None, None))
                new_entries.append(entry)

        last_holdings_with_currencies = holdings_with_currencies
        date = date_utils.next_month(date)

    # Ensure that the accounts we're going to use to book the postings exist, by
    # creating open entries for those that we generated that weren't already
    # existing accounts.
    new_accounts = {
        posting.account
        for entry in new_entries for posting in entry.postings
    }
    open_entries = getters.get_account_open_close(entries)
    new_open_entries = []
    for index, account_ in enumerate(sorted(new_accounts)):
        if account_ not in open_entries:
            meta = data.new_metadata(meta["filename"], index)
            open_entry = data.Open(meta, new_entries[0].date, account_, None,
                                   None)
            new_open_entries.append(open_entry)

    return (entries + new_open_entries + new_entries, errors)
def add_unrealized_gains(entries, options_map, subaccount=None):
    """Insert entries for unrealized capital gains.

    This function inserts entries that represent unrealized gains, at the end of
    the available history. It returns a new list of entries, with the new gains
    inserted. It replaces the account type with an entry in an income account.
    Optionally, it can book the gain in a subaccount of the original and income
    accounts.

    Args:
      entries: A list of data directives.
      options_map: A dict of options, that confirms to beancount.parser.options.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of entries, which includes the new unrealized capital gains entries
      at the end, and a list of errors. The new list of entries is still sorted.
    """
    errors = []
    meta = data.new_metadata('<unrealized_gains>', 0)

    account_types = options.get_account_types(options_map)

    # Assert the subaccount name is in valid format.
    if subaccount:
        validation_account = account.join(account_types.assets, subaccount)
        if not account.is_valid(validation_account):
            errors.append(
                UnrealizedError(meta,
                                "Invalid subaccount name: '{}'".format(subaccount),
                                None))
            return entries, errors

    if not entries:
        return (entries, errors)

    # Group positions by (account, cost, cost_currency).
    price_map = prices.build_price_map(entries)

    new_entries = []

    # Start at the first month after our first transaction
    date = date_utils.next_month(entries[0].date)
    last_month = date_utils.next_month(entries[-1].date)
    last_holdings_with_currencies = None
    while date <= last_month:
        date_entries, holdings_with_currencies, date_errors = add_unrealized_gains_at_date(
            entries, new_entries, account_types.income, price_map, date, meta,
            subaccount)
        new_entries.extend(date_entries)
        errors.extend(date_errors)

        if last_holdings_with_currencies:
            for account_, cost_currency, currency in last_holdings_with_currencies - holdings_with_currencies:
                # Create a negation transaction specifically to mark that all gains have been realized
                if subaccount:
                    account_ = account.join(account_, subaccount)

                latest_unrealized_entry = find_previous_unrealized_transaction(new_entries, account_, cost_currency, currency)
                if not latest_unrealized_entry:
                    continue
                entry = data.Transaction(data.new_metadata(meta["filename"], lineno=999,
                                         kvlist={'prev_currency': currency}), date,
                                         flags.FLAG_UNREALIZED, None, 'Clear unrealized gains/losses of {}'.format(currency), set(), set(), [])

                # Negate the previous transaction because of unrealized gains are now 0
                for posting in latest_unrealized_entry.postings[:2]:
                    entry.postings.append(
                        data.Posting(
                            posting.account,
                            -posting.units,
                            None,
                            None,
                            None,
                            None))
                new_entries.append(entry)


        last_holdings_with_currencies = holdings_with_currencies
        date = date_utils.next_month(date)

    # Ensure that the accounts we're going to use to book the postings exist, by
    # creating open entries for those that we generated that weren't already
    # existing accounts.
    new_accounts = {posting.account
                    for entry in new_entries
                    for posting in entry.postings}
    open_entries = getters.get_account_open_close(entries)
    new_open_entries = []
    for index, account_ in enumerate(sorted(new_accounts)):
        if account_ not in open_entries:
            meta = data.new_metadata(meta["filename"], index)
            open_entry = data.Open(meta, new_entries[0].date, account_, None, None)
            new_open_entries.append(open_entry)

    return (entries + new_open_entries + new_entries, errors)